Changeset 3454648
- Timestamp:
- 02/05/2026 01:11:59 PM (13 days ago)
- Location:
- media-tracker
- Files:
-
- 113 added
- 18 edited
-
tags/1.3.0 (added)
-
tags/1.3.0/assets (added)
-
tags/1.3.0/assets/dist (added)
-
tags/1.3.0/assets/dist/css (added)
-
tags/1.3.0/assets/dist/css/mt-admin.css (added)
-
tags/1.3.0/assets/dist/css/mt-admin.css.map (added)
-
tags/1.3.0/assets/dist/css/pro-lock.css (added)
-
tags/1.3.0/assets/dist/css/pro-lock.css.map (added)
-
tags/1.3.0/assets/dist/images (added)
-
tags/1.3.0/assets/dist/images/logo.svg (added)
-
tags/1.3.0/assets/dist/images/mt-pro-1.webp (added)
-
tags/1.3.0/assets/dist/images/mt-pro-2.webp (added)
-
tags/1.3.0/assets/dist/images/mt-pro-3.webp (added)
-
tags/1.3.0/assets/dist/images/mt-pro-5.webp (added)
-
tags/1.3.0/assets/dist/images/youtube-thumb.webp (added)
-
tags/1.3.0/assets/dist/js (added)
-
tags/1.3.0/assets/dist/js/mt-admin.js (added)
-
tags/1.3.0/assets/dist/js/tab.js (added)
-
tags/1.3.0/assets/src (added)
-
tags/1.3.0/assets/src/images (added)
-
tags/1.3.0/assets/src/images/logo.svg (added)
-
tags/1.3.0/assets/src/images/mt-pro-1.webp (added)
-
tags/1.3.0/assets/src/images/mt-pro-2.webp (added)
-
tags/1.3.0/assets/src/images/mt-pro-3.webp (added)
-
tags/1.3.0/assets/src/images/mt-pro-5.webp (added)
-
tags/1.3.0/assets/src/images/youtube-thumb.webp (added)
-
tags/1.3.0/assets/src/js (added)
-
tags/1.3.0/assets/src/js/mt-admin.js (added)
-
tags/1.3.0/assets/src/js/tab.js (added)
-
tags/1.3.0/assets/src/scss (added)
-
tags/1.3.0/assets/src/scss/mt-admin.scss (added)
-
tags/1.3.0/assets/src/scss/pro-lock.scss (added)
-
tags/1.3.0/composer.json (added)
-
tags/1.3.0/gulp.config.js (added)
-
tags/1.3.0/gulpfile.babel.js (added)
-
tags/1.3.0/includes (added)
-
tags/1.3.0/includes/Admin (added)
-
tags/1.3.0/includes/Admin.php (added)
-
tags/1.3.0/includes/Admin/Duplicate_Images.php (added)
-
tags/1.3.0/includes/Admin/Media_Usage.php (added)
-
tags/1.3.0/includes/Admin/Menu.php (added)
-
tags/1.3.0/includes/Admin/PluginMeta.php (added)
-
tags/1.3.0/includes/Admin/Unused_Media_List.php (added)
-
tags/1.3.0/includes/Admin/views (added)
-
tags/1.3.0/includes/Admin/views/media-tracker.php (added)
-
tags/1.3.0/includes/Admin/views/tabs (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-documents.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-duplicates.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-external-storage.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-multisite.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-optimization.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-overview.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-security.php (added)
-
tags/1.3.0/includes/Admin/views/tabs/tab-unused-media.php (added)
-
tags/1.3.0/includes/Admin/views/unused-media-list.php (added)
-
tags/1.3.0/includes/Assets.php (added)
-
tags/1.3.0/includes/Cron_Schedules.php (added)
-
tags/1.3.0/includes/Installer.php (added)
-
tags/1.3.0/includes/Media_Tracker_i18n.php (added)
-
tags/1.3.0/includes/functions.php (added)
-
tags/1.3.0/languages (added)
-
tags/1.3.0/languages/media-tracker.pot (added)
-
tags/1.3.0/media-tracker.php (added)
-
tags/1.3.0/package.json (added)
-
tags/1.3.0/readme.txt (added)
-
tags/1.3.0/vendor (added)
-
tags/1.3.0/vendor/autoload.php (added)
-
tags/1.3.0/vendor/composer (added)
-
tags/1.3.0/vendor/composer/ClassLoader.php (added)
-
tags/1.3.0/vendor/composer/InstalledVersions.php (added)
-
tags/1.3.0/vendor/composer/LICENSE (added)
-
tags/1.3.0/vendor/composer/autoload_classmap.php (added)
-
tags/1.3.0/vendor/composer/autoload_files.php (added)
-
tags/1.3.0/vendor/composer/autoload_namespaces.php (added)
-
tags/1.3.0/vendor/composer/autoload_psr4.php (added)
-
tags/1.3.0/vendor/composer/autoload_real.php (added)
-
tags/1.3.0/vendor/composer/autoload_static.php (added)
-
tags/1.3.0/vendor/composer/installed.json (added)
-
tags/1.3.0/vendor/composer/installed.php (added)
-
trunk/assets/dist/css/mt-admin.css (modified) (1 diff)
-
trunk/assets/dist/css/mt-admin.css.map (modified) (1 diff)
-
trunk/assets/dist/css/pro-lock.css (added)
-
trunk/assets/dist/css/pro-lock.css.map (added)
-
trunk/assets/dist/images (added)
-
trunk/assets/dist/images/logo.svg (added)
-
trunk/assets/dist/images/mt-pro-1.webp (added)
-
trunk/assets/dist/images/mt-pro-2.webp (added)
-
trunk/assets/dist/images/mt-pro-3.webp (added)
-
trunk/assets/dist/images/mt-pro-5.webp (added)
-
trunk/assets/dist/images/youtube-thumb.webp (added)
-
trunk/assets/dist/js/tab.js (added)
-
trunk/assets/src/images (added)
-
trunk/assets/src/images/logo.svg (added)
-
trunk/assets/src/images/mt-pro-1.webp (added)
-
trunk/assets/src/images/mt-pro-2.webp (added)
-
trunk/assets/src/images/mt-pro-3.webp (added)
-
trunk/assets/src/images/mt-pro-5.webp (added)
-
trunk/assets/src/images/youtube-thumb.webp (added)
-
trunk/assets/src/js/tab.js (added)
-
trunk/assets/src/scss/mt-admin.scss (modified) (2 diffs)
-
trunk/assets/src/scss/pro-lock.scss (added)
-
trunk/composer.json (modified) (1 diff)
-
trunk/includes/Admin.php (modified) (1 diff)
-
trunk/includes/Admin/Duplicate_Images.php (modified) (10 diffs)
-
trunk/includes/Admin/Media_Usage.php (modified) (6 diffs)
-
trunk/includes/Admin/Menu.php (modified) (15 diffs)
-
trunk/includes/Admin/PluginMeta.php (added)
-
trunk/includes/Admin/Unused_Media_List.php (modified) (40 diffs)
-
trunk/includes/Admin/views/media-tracker.php (added)
-
trunk/includes/Admin/views/tabs (added)
-
trunk/includes/Admin/views/tabs/tab-documents.php (added)
-
trunk/includes/Admin/views/tabs/tab-duplicates.php (added)
-
trunk/includes/Admin/views/tabs/tab-external-storage.php (added)
-
trunk/includes/Admin/views/tabs/tab-multisite.php (added)
-
trunk/includes/Admin/views/tabs/tab-optimization.php (added)
-
trunk/includes/Admin/views/tabs/tab-overview.php (added)
-
trunk/includes/Admin/views/tabs/tab-security.php (added)
-
trunk/includes/Admin/views/tabs/tab-unused-media.php (added)
-
trunk/includes/Admin/views/unused-media-list.php (modified) (13 diffs)
-
trunk/includes/Assets.php (added)
-
trunk/includes/Cron_Schedules.php (added)
-
trunk/includes/Installer.php (modified) (1 diff)
-
trunk/includes/functions.php (added)
-
trunk/languages/media-tracker.pot (modified) (5 diffs)
-
trunk/media-tracker.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/vendor/composer/InstalledVersions.php (modified) (5 diffs)
-
trunk/vendor/composer/autoload_files.php (added)
-
trunk/vendor/composer/autoload_real.php (modified) (1 diff)
-
trunk/vendor/composer/autoload_static.php (modified) (1 diff)
-
trunk/vendor/composer/installed.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
media-tracker/trunk/assets/dist/css/mt-admin.css
r3432010 r3454648 1 .mediatracker-usage-table{background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-box-shadow:0 0 10px rgba(0,0,0,.1);box-shadow:0 0 10px rgba(0,0,0,.1);margin-top:20px;padding:10px}.mediatracker-usage-table table{border-collapse:collapse;width:100%}.mediatracker-usage-table table td,.mediatracker-usage-table table th{border-bottom:1px solid #ddd;padding:10px;text-align:left;text-transform:capitalize}.mediatracker-usage-table table th{background-color:#f4f4f4}.mediatracker-usage-table table tr:nth-child(2n){background-color:#f9f9f9}.mediatracker-usage-table table tr:hover{background-color:#f1f1f1}.mediatracker-usage-table table a{color:#0073aa;text-decoration:none}.mediatracker-usage-table table a:hover{text-decoration:underline}.unused-media-list .wp-list-table td strong{display:block;font-size:14px;margin-bottom:.2em}.unused-media-list .wp-list-table .media-icon{float:left;margin:0 9px 0 0;min-height:60px}.unused-media-list .wp-list-table .media-icon img{height:60px;width:60px}.unused-media-list .wp-list-table.fixed{table-layout:inherit}#mt-feedback-modal{-webkit-box-pack:center;-ms-flex-pack:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:rgba(0,0,0,.3);display:none;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:10000}#mt-feedback-modal .mt-feedback-modal-content{background:#fff;border-radius:8px;-webkit-box-shadow:0 4px 8px rgba(0,0,0,.2);box-shadow:0 4px 8px rgba(0,0,0,.2);margin:10% auto;max-width:600px;padding:30px;position:relative;text-align:center;width:80%}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header{text-align:left}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header h3{color:#333;font-size:1.5em;line-height:normal;margin-top:0}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header .close{cursor:pointer;font-size:1.5em;position:absolute;right:10px;top:10px}#mt-feedback-modal .mt-feedback-modal-content .mt-feedback-modal-body textarea{border:1px solid #ddd;border-radius:4px;font-size:1em;height:120px;margin:20px 0;padding:10px;resize:vertical;width:100%}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer{-webkit-box-align:start;-ms-flex-align:start;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:flex-start;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button{background-color:#007cba;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:1em;margin:5px;padding:10px 20px;-webkit-transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,transform .3s;transition:background-color .3s,transform .3s,-webkit-transform .3s}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:hover{background-color:#005a87;-webkit-transform:scale(1.05);transform:scale(1.05)}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:focus{outline:none}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button#mt-skip-feedback{background:transparent;border:1px solid #2271b1;color:#2271b1}.broken-link-checker .post_title{padding-left:15px!important}.broken-link-checker #post_type{padding:0}.wrap.broken-link-checker .wp-heading-inline,.wrap.unused-media-list .wp-heading-inline{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#28a745;border-radius:5px;color:#fff;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;gap:20px;margin-bottom:10px;margin-top:15px;padding:20px;width:calc(100% - 40px)}.wrap.broken-link-checker .wp-heading-inline svg,.wrap.unused-media-list .wp-heading-inline svg{height:40px;width:40px}.wrap.broken-link-checker .wp-filter,.wrap.unused-media-list .wp-filter{margin:10px 0}.wrap.broken-link-checker .notice h2,.wrap.unused-media-list .notice h2{margin-bottom:0}.wrap.broken-link-checker table.widefat,.wrap.unused-media-list table.widefat{margin-top:20px;table-layout:inherit;width:100%}.wrap.broken-link-checker table.widefat td,.wrap.broken-link-checker table.widefat th,.wrap.unused-media-list table.widefat td,.wrap.unused-media-list table.widefat th{padding:10px;text-align:left}.wrap.broken-link-checker table.widefat td.status,.wrap.broken-link-checker table.widefat th.status,.wrap.unused-media-list table.widefat td.status,.wrap.unused-media-list table.widefat th.status{color:red;font-weight:700}.media-toolbar-wrap.wp-filter{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;gap:20px;justify-content:space-between;padding:0 15px}.media-toolbar-wrap.wp-filter .search-form input[type=search]{width:215px}.unused-image-found h2{font-size:24px;font-weight:300}.unused-image-found h2 span{color:#cf0000;font-size:28px;font-weight:700}.replace-broken-link input{width:100%}#clear-broken-links-transient{font-size:14px;font-weight:500;padding:8px 30px;position:absolute}#success-message{color:green;display:none;font-size:16px;left:230px;margin-top:15px;position:absolute}.wp-list-table #usage_count{width:130px}1 #mt-feedback-modal{-webkit-box-pack:center;-ms-flex-pack:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:rgba(0,0,0,.3);display:none;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:10000}#mt-feedback-modal .mt-feedback-modal-content{background:#fff;border-radius:8px;-webkit-box-shadow:0 4px 8px rgba(0,0,0,.2);box-shadow:0 4px 8px rgba(0,0,0,.2);margin:10% auto;max-width:600px;padding:30px;position:relative;text-align:center;width:80%}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header{text-align:left}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header h3{color:#333;font-size:1.5em;line-height:normal;margin-top:0}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header .close{cursor:pointer;font-size:1.5em;position:absolute;right:10px;top:10px}#mt-feedback-modal .mt-feedback-modal-content .mt-feedback-modal-body textarea{border:1px solid #ddd;border-radius:4px;font-size:1em;height:120px;margin:20px 0;padding:10px;resize:vertical;width:100%}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer{-webkit-box-align:start;-ms-flex-align:start;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:flex-start;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button{background-color:#007cba;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:1em;margin:5px;padding:10px 20px;-webkit-transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,transform .3s;transition:background-color .3s,transform .3s,-webkit-transform .3s}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:hover{background-color:#005a87;-webkit-transform:scale(1.05);transform:scale(1.05)}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:focus{outline:none}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button#mt-skip-feedback{background:transparent;border:1px solid #2271b1;color:#2271b1}.broken-link-checker .post_title{padding-left:15px!important}.broken-link-checker #post_type{padding:0}.wrap.broken-link-checker .wp-heading-inline,.wrap.unused-media-list .wp-heading-inline{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#28a745;border-radius:5px;color:#fff;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;gap:20px;margin-bottom:10px;margin-top:15px;padding:20px;width:calc(100% - 40px)}.wrap.broken-link-checker .wp-heading-inline svg,.wrap.unused-media-list .wp-heading-inline svg{height:40px;width:40px}.media-tracker-layout{background-color:#f8fafc;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-sizing:inherit;box-sizing:inherit;color:#1e293b;margin:24px 0 0;min-height:100vh;width:98.8%}.media-tracker-layout,.media-tracker-layout h2{display:-webkit-box;display:-ms-flexbox;display:flex;padding:0}.media-tracker-layout h2{-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;margin:0}.media-tracker-layout aside{-webkit-box-orient:vertical;-webkit-box-direction:normal;background:#0f172a;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:12px;width:260px}.media-tracker-layout aside .version{padding:15px;text-align:center}.media-tracker-layout .logo{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:19.2px;font-size:1.2rem;font-weight:700;gap:8px;margin:10px 0 20px}.media-tracker-layout nav{-webkit-box-flex:1;-ms-flex:1;flex:1}.media-tracker-layout nav ul{list-style:none;margin:0}.media-tracker-layout nav li{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:start;-ms-flex-pack:start;align-items:center;border-radius:4px;color:#fff;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:14px;gap:10px;justify-content:flex-start;letter-spacing:.1px;margin:0 0 3px;padding:13px 10px;-webkit-transition:.25s;transition:.25s}.media-tracker-layout nav li.active,.media-tracker-layout nav li:hover{background:#6366f1;color:#fff}.media-tracker-layout nav li i{text-align:left;width:20px}.media-tracker-layout ul li a{border:none;color:#fff;gap:10px;outline:none;padding:15px;text-decoration:none;width:100%}.media-tracker-layout ul li a,.media-tracker-layout ul li a small{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout ul li a small{margin-left:auto}.media-tracker-layout ul li a:focus{-webkit-box-shadow:none;box-shadow:none;outline:none}.media-tracker-layout ul li.license,.media-tracker-layout ul li.multisite,.media-tracker-layout ul li.settings{padding:15px!important}.media-tracker-layout ul li:last-child{padding:0}.media-tracker-layout ul li:hover a{color:#fff}.media-tracker-layout main{-webkit-box-flex:1;background:#fff;-ms-flex:1;flex:1;overflow-y:auto;padding:2rem}.media-tracker-layout header{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;margin-bottom:2rem}.media-tracker-layout .status-badge{background:#dcfce7;border-radius:20px;color:#166534;font-size:12px;font-weight:600;padding:4px 12px}.media-tracker-layout h1{font-size:22px;font-weight:600;letter-spacing:-.01em;margin:0}.media-tracker-layout .stats-grid{display:grid;gap:24px;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin-bottom:1.5rem}.media-tracker-layout .card{background:#fff;border:1px solid #e2e8f0;border-radius:10px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.05);box-shadow:0 1px 3px rgba(0,0,0,.05);margin:0;max-width:100%;padding:1.5rem}.media-tracker-layout .card table .btn{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;font-size:12px;width:80px}.media-tracker-layout .card h3{color:#313335;font-size:16px;margin-bottom:12px;margin-top:0}.media-tracker-layout .card h3 i{color:#6366f1}.media-tracker-layout .card .value{display:block;font-size:18px;font-weight:700;line-height:normal;margin-bottom:3px}.media-tracker-layout .progress-bar{background:#e5e7eb;border-radius:4px;height:8px;margin-top:10px;overflow:hidden}.media-tracker-layout .progress-fill{background:#6366f1;border-radius:4px;height:100%}.media-tracker-layout .section-title{font-size:16px}.media-tracker-layout .grid-two{display:grid;gap:24px;gap:1.5rem;grid-template-columns:2.05fr 1fr}.media-tracker-layout .grid-three{display:grid;gap:24px;gap:1.5rem;grid-template-columns:1fr 1fr 1fr}.media-tracker-layout .setting-item{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border-bottom:1px solid #e2e8f0;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;padding:12px 0}.media-tracker-layout .setting-item p{margin:0}.media-tracker-layout .setting-item:last-child{border:none}.media-tracker-layout .switch{display:inline-block;height:22px;position:relative;width:44px}.media-tracker-layout .switch input{height:0;opacity:0;width:0}.media-tracker-layout .slider{background-color:#cbd5f5;border-radius:34px;bottom:0;cursor:pointer;left:0;position:absolute;right:0;top:0;-webkit-transition:.25s;transition:.25s}.media-tracker-layout .slider:before{background-color:#fff;border-radius:50%;bottom:3px;content:"";height:16px;left:3px;position:absolute;-webkit-transition:.25s;transition:.25s;width:16px}.media-tracker-layout input:checked+.slider{background-color:#6366f1}.media-tracker-layout input:checked+.slider:before{-webkit-transform:translateX(22px);transform:translateX(22px)}.media-tracker-layout .btn{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;border:none;border-radius:6px;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:14px;font-weight:600;gap:5px;justify-content:center;padding:12px 20px;text-decoration:none;-webkit-transition:.2s;transition:.2s}.media-tracker-layout .btn i{font-size:14px;height:auto}.media-tracker-layout .btn-primary{background:#6366f1;color:#fff}.media-tracker-layout .btn-primary:hover{background:#4f46e5}.media-tracker-layout .btn-outline{background:transparent;border:1px solid #e2e8f0;color:#1e293b}.media-tracker-layout .btn-outline:hover{background:#f1f5f9}.media-tracker-layout .btn-danger{background:#ef4444;color:#fff}.media-tracker-layout table{border-collapse:collapse;margin-top:1rem;width:100%}.media-tracker-layout th{color:#64748b;font-size:14px;padding:12px;text-align:left}.media-tracker-layout th:last-child{text-align:center}.media-tracker-layout td{border-bottom:1px solid #c3c4c7;font-size:14px;padding:12px}.media-tracker-layout tr:last-child td{border-bottom:none}.media-tracker-layout .tag{border-radius:4px;font-size:11px;font-weight:700;padding:2px 8px;text-transform:uppercase}.media-tracker-layout .tag-unused{background:#fee2e2;color:#991b1b}.media-tracker-layout .tag-duplicate{background:#fef3c7;color:#92400e}.media-tracker-layout .tab-content{display:none}.media-tracker-layout .tab-content.active{display:block}.media-tracker-layout .page-subtitle{color:#64748b;font-size:13px;margin-bottom:0;margin-top:10px}.media-tracker-layout .stacked{-webkit-box-orient:vertical;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:10px;margin-top:10px}.media-tracker-layout .inline-actions{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:end;-ms-flex-pack:end;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:10px;justify-content:flex-end}.media-tracker-layout .modal-backdrop{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;background:rgba(15,23,42,.55);display:none;inset:0;justify-content:center;position:fixed;z-index:50}.media-tracker-layout .modal-backdrop.active{display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .modal{background:#fff;border:1px solid #e2e8f0;border-radius:12px;-webkit-box-shadow:0 20px 40px rgba(15,23,42,.3);box-shadow:0 20px 40px rgba(15,23,42,.3);max-width:94%;padding:1.5rem;width:480px}.media-tracker-layout .modal-header{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:1rem}.media-tracker-layout .modal-header,.media-tracker-layout .modal-title{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .modal-title{font-size:18px;font-weight:600;gap:8px}.media-tracker-layout .modal-close{background:transparent;border:none;color:#64748b;cursor:pointer;font-size:18px}.media-tracker-layout .modal-body{-webkit-box-orient:vertical;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:12px;margin-bottom:1.5rem}.media-tracker-layout .modal-body label{display:block;font-size:13px;margin-bottom:4px}.media-tracker-layout .modal-body input,.media-tracker-layout .modal-body select{border:1px solid #e2e8f0;border-radius:6px;font-size:13px;padding:8px 10px;width:100%}.media-tracker-layout .modal-body input:focus,.media-tracker-layout .modal-body select:focus{border-color:#6366f1;-webkit-box-shadow:0 0 0 1px rgba(99,102,241,.25);box-shadow:0 0 0 1px rgba(99,102,241,.25);outline:none}.media-tracker-layout .modal-hint{color:#64748b;font-size:11px}.media-tracker-layout .modal-footer{-webkit-box-pack:end;-ms-flex-pack:end;display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;justify-content:flex-end}.media-tracker-layout .mediatracker-usage-table{margin-top:15px}.media-tracker-layout .mediatracker-usage-table table.wp-list-table td,.media-tracker-layout .mediatracker-usage-table table.wp-list-table th{vertical-align:middle}.media-tracker-layout .mediatracker-usage-table table.wp-list-table .button{margin-right:4px}.media-tracker-layout .unused-media-list .wp-list-table td strong{display:block;font-size:14px;margin-bottom:.2em}.media-tracker-layout .unused-media-list .wp-list-table .media-icon{float:left;margin:0 9px 0 0;min-height:60px}.media-tracker-layout .unused-media-list .wp-list-table .media-icon img{height:60px;width:60px}.media-tracker-layout .unused-media-list .wp-list-table.fixed{table-layout:inherit}.media-tracker-layout .unused-media-list .search-box{margin:0}.media-tracker-layout .unused-media-list .wp-filter{margin:0 0 20px}.media-tracker-layout .unused-media-list .notice h2{margin-bottom:0}.media-tracker-layout .unused-media-list table.widefat{margin-top:20px;table-layout:inherit;width:100%}.media-tracker-layout .unused-media-list table.widefat td,.media-tracker-layout .unused-media-list table.widefat th{padding:10px;text-align:left}.media-tracker-layout .unused-media-list table.widefat td.status,.media-tracker-layout .unused-media-list table.widefat th.status{color:red;font-weight:700}.media-tracker-layout .media-toolbar-wrap.wp-filter{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border:none;-webkit-box-shadow:none;box-shadow:none;display:-webkit-box;display:-ms-flexbox;display:flex;gap:20px;justify-content:space-between}.media-tracker-layout .media-toolbar-wrap.wp-filter .search-form input[type=search]{width:215px}.media-tracker-layout .unused-image-found h2{font-size:20px;font-weight:400}.media-tracker-layout .unused-image-found h2 span{color:#6366f1;font-size:20px;font-weight:700}.media-tracker-layout .replace-broken-link input{width:100%}.media-tracker-layout #clear-broken-links-transient{font-size:14px;font-weight:500;padding:8px 30px;position:absolute}.media-tracker-layout #success-message{color:green;display:none;font-size:16px;left:230px;margin-top:15px;position:absolute}.media-tracker-layout .wp-list-table #usage_count{width:130px}.media-tracker-layout #mt-duplicate-form table tbody td:last-child,.media-tracker-layout #mt-duplicate-form table tr th:last-child{text-align:center}.media-tracker-layout #mt-duplicate-form table .check-column{background:transparent;border-bottom:1px solid #c3c4c7;padding:16px 3px;width:3.2em}.media-tracker-layout #mt-duplicate-form .duplicate-media-footer{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;gap:30px;justify-content:space-between;margin-top:15px}.media-tracker-layout #mt-duplicate-form .tablenav-pages .paging-input{margin:0 15px}.media-tracker-layout .duplicate-media-count h2{color:#6366f1;font-size:20px;font-weight:700}.media-tracker-layout .duplicate-media-count h2 span{color:#1d2327;font-weight:400}.media-tracker-layout .media-header{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;margin-bottom:30px}.media-tracker-layout #tab-unused-media .tablenav.bottom{margin-top:15px}.media-tracker-layout .mt-overview-table{background:#fff;border-collapse:separate;border-radius:8px;border-spacing:0;-webkit-box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);margin-top:20px;overflow:hidden;width:100%}.media-tracker-layout .mt-overview-table thead th{background-color:#f8fafc;border-bottom:1px solid #e2e8f0;color:#475569;font-size:11px;font-weight:600;letter-spacing:.05em;padding:16px;text-align:left;text-transform:uppercase}.media-tracker-layout .mt-overview-table thead tr:first-child th:first-child{border-top-left-radius:8px}.media-tracker-layout .mt-overview-table thead tr:first-child th:last-child{border-top-right-radius:8px}.media-tracker-layout .mt-overview-table tbody tr{-webkit-transition:background-color .2s;transition:background-color .2s}.media-tracker-layout .mt-overview-table tbody tr:nth-child(2n){background-color:#f8fafc}.media-tracker-layout .mt-overview-table tbody tr:hover{background-color:#f1f5f9}.media-tracker-layout .mt-overview-table tbody tr:last-child td{border-bottom:none}.media-tracker-layout .mt-overview-table tbody td{border-bottom:1px solid #f1f5f9;color:#334155;font-size:14px;padding:16px;vertical-align:middle}.media-tracker-layout .mt-overview-table tbody td:last-child{text-align:center}.media-tracker-layout .mt-overview-table tbody td strong{color:#0f172a;font-weight:500}.media-tracker-layout .mt-overview-table tbody td small{color:#94a3b8;display:block;margin-top:4px}.media-tracker-layout .mt-overview-table tbody tr:last-child td:first-child{border-bottom-left-radius:8px}.media-tracker-layout .mt-overview-table tbody tr:last-child td:last-child{border-bottom-right-radius:8px}.media-tracker-layout .mt-flex-col-end{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:8px}.media-tracker-layout .mt-flex-center{gap:10px}.media-tracker-layout .mt-flex-center,.media-tracker-layout .mt-stat-title{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .mt-stat-title{gap:8px}.media-tracker-layout .mt-icon-indigo{color:#6366f1}.media-tracker-layout .mt-icon-amber{color:#f59e0b}.media-tracker-layout .mt-stat-subtitle{color:#64748b;font-size:12px}.media-tracker-layout .mt-mt-10{margin-top:10px}.media-tracker-layout .mt-helper-text{color:#64748b;font-size:12px}.media-tracker-layout .mt-btn-sm{font-size:12px!important;padding:6px 12px!important}.media-tracker-layout .mt-btn-xs{padding:5px 10px!important}.media-tracker-layout .mt-mime-item{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f8fafc;border-radius:8px;display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;padding:10px}.media-tracker-layout .mt-mime-icon{color:#6366f1;text-align:center;width:20px}.media-tracker-layout .mt-flex-1{-webkit-box-flex:1;-ms-flex:1;flex:1}.media-tracker-layout .mt-font-medium{font-size:14px;font-weight:500}.media-tracker-layout .mt-empty-state{color:#64748b;font-size:12px;padding:20px;text-align:center}.media-tracker-layout .mt-mt-6{margin-top:1.5rem}.media-tracker-layout .mt-empty-state-large{color:#64748b;padding:40px;text-align:center}.media-tracker-layout .mt-success-icon-large{color:#10b981;font-size:48px;height:48px;margin-bottom:15px;width:48px}.media-tracker-layout .mt-tag-success{background:#10b981;color:#fff}.media-tracker-layout .mt-btn-xs-clean{padding:5px 10px!important;text-decoration:none}.media-tracker-layout .mt-chart-icon-large{color:#cbd5e1;font-size:48px;height:48px;margin-bottom:15px;width:48px}.media-tracker-layout .mt-mb-3{margin-bottom:12px}.media-tracker-layout .mt-progress-text{color:#64748b;display:none;font-size:11px;margin-left:8px;min-width:40px;text-align:right}.media-tracker-layout .mt-status-text{display:none;margin-left:4px}.media-tracker-layout .mt-progress-track{-webkit-box-flex:1;background:-webkit-gradient(linear,left top,right top,from(#eef2ff),to(#e2e8f0));background:linear-gradient(90deg,#eef2ff,#e2e8f0);border-radius:999px;-webkit-box-shadow:0 0 0 1px rgba(148,163,184,.4);box-shadow:0 0 0 1px rgba(148,163,184,.4);display:none;-ms-flex:1;flex:1;height:15px;max-width:100%;overflow:hidden}.media-tracker-layout .mt-progress-bar-fill{background:#6366f1;height:100%;width:0}.media-tracker-layout .mt-v-middle{vertical-align:middle}.media-tracker-layout .mt-text-center{text-align:center}.media-tracker-layout .mt-mr-1{margin-right:4px}.media-tracker-layout .mt-thumb-img{height:auto;width:60px}.media-tracker-layout .mt-link-clean{font-weight:500;text-decoration:none}@media(max-width:960px){.media-tracker-layout{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.media-tracker-layout aside{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:center;-ms-flex-direction:row;flex-direction:row;justify-content:space-between;padding:1rem 1.5rem;width:100%}.media-tracker-layout nav ul{display:-webkit-box;display:-ms-flexbox;display:flex;overflow-x:auto}.media-tracker-layout nav li{white-space:nowrap}}.media-video-featured-card{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;background:#fff;border:1px solid #e2e8f0;border-radius:12px;-webkit-box-shadow:0 4px 6px -1px rgba(0,0,0,.05);box-shadow:0 4px 6px -1px rgba(0,0,0,.05);cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;overflow:hidden;-webkit-transition:all .3s ease;transition:all .3s ease;width:32%}.media-video-featured-card:hover{border-color:#cbd5e1;-webkit-box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);-webkit-transform:translateY(-2px);transform:translateY(-2px)}.media-video-featured-card:hover .video-thumbnail{opacity:.6}.media-video-featured-card:hover .play-icon{background:#fff;-webkit-box-shadow:0 0 0 8px hsla(0,0%,100%,.3);box-shadow:0 0 0 8px hsla(0,0%,100%,.3);-webkit-transform:scale(1.1);transform:scale(1.1)}.media-video-featured-card .video-thumbnail-wrapper{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;background:#000;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center;overflow:hidden;position:relative}.media-video-featured-card .video-thumbnail{height:100%;-o-object-fit:cover;object-fit:cover;opacity:.8;-webkit-transition:opacity .3s ease;transition:opacity .3s ease;width:100%}.media-video-featured-card .play-button-overlay{height:100%;left:0;position:absolute;top:0;width:100%;z-index:2}.media-video-featured-card .play-button-overlay,.media-video-featured-card .play-icon{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center}.media-video-featured-card .play-icon{background:hsla(0,0%,100%,.9);border-radius:50%;-webkit-box-shadow:0 0 0 0 hsla(0,0%,100%,.7);box-shadow:0 0 0 0 hsla(0,0%,100%,.7);height:60px;-webkit-transition:all .3s cubic-bezier(.175,.885,.32,1.275);transition:all .3s cubic-bezier(.175,.885,.32,1.275);width:60px}.media-video-featured-card .play-icon .dashicons{color:#6366f1;font-size:32px;height:32px;margin-left:4px;width:32px}.media-video-featured-card .video-card-content{-webkit-box-flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex:1;flex:1;-ms-flex-direction:column;flex-direction:column;justify-content:center;padding:2rem}.media-video-featured-card .video-card-content h4{color:#1e293b;font-size:20px;font-size:1.25rem;font-weight:600;margin:0 0 .75rem}.mt-video-modal-backdrop{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;backdrop-filter:blur(4px);background:rgba(15,23,42,.75);display:none;inset:0;justify-content:center;opacity:0;position:fixed;-webkit-transition:opacity .3s ease;transition:opacity .3s ease;z-index:100000}.mt-video-modal-backdrop.active{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.mt-video-modal-backdrop.active .mt-video-modal{-webkit-transform:scale(1);transform:scale(1)}.mt-video-modal{background:#fff;border-radius:16px;-webkit-box-shadow:0 25px 50px -12px rgba(0,0,0,.25);box-shadow:0 25px 50px -12px rgba(0,0,0,.25);max-width:800px;overflow:hidden;-webkit-transform:scale(.95);transform:scale(.95);-webkit-transition:-webkit-transform .3s cubic-bezier(.34,1.56,.64,1);transition:-webkit-transform .3s cubic-bezier(.34,1.56,.64,1);transition:transform .3s cubic-bezier(.34,1.56,.64,1);transition:transform .3s cubic-bezier(.34,1.56,.64,1),-webkit-transform .3s cubic-bezier(.34,1.56,.64,1);width:90%}.mt-video-modal-header{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;padding:1rem 1.5rem}.mt-video-modal-header h3{color:#334155;font-size:17.6px;font-size:1.1rem;margin:0}.mt-video-modal-close{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;background:transparent;border:none;border-radius:4px;color:#64748b;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center;padding:4px;-webkit-transition:background .2s;transition:background .2s}.mt-video-modal-close:hover{background:#e2e8f0;color:#ef4444}.mt-video-modal-body{background:#000;padding:0}.mt-responsive-video-wrapper{height:0;overflow:hidden;padding-bottom:56.25%;position:relative}.mt-responsive-video-wrapper iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@media(max-width:768px){.media-video-featured-card{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.media-video-featured-card .video-thumbnail-wrapper{height:200px;width:100%}.media-video-featured-card .video-card-content{padding:1.5rem}} 2 2 /*# sourceMappingURL=mt-admin.css.map */ -
media-tracker/trunk/assets/dist/css/mt-admin.css.map
r3159474 r3454648 1 {"version":3,"sources":["mt-admin.scss"],"names":[],"mappings":"AA KA,0BAKI,qBAAA,CAHA,qBAAA,CACA,iBAAA,CAGA,0CAAA,CAAA,kCAAA,CALA,eAAA,CAGA,YAEA,CAEA,gCAEI,wBAAA,CADA,UACA,CAEA,sEAGI,4BAAA,CAFA,YAAA,CACA,eAAA,CAEA,yBAAA,CAGJ,mCACI,wBAAA,CAGJ,iDACI,wBAAA,CAGJ,yCACI,wBAAA,CAGJ,kCACI,aAAA,CACA,oBAAA,CAEA,wCACI,yBAAA,CASR,4CAEI,aAAA,CAEA,cAAA,CADA,kBACA,CAGJ,8CACI,UAAA,CAEA,gBAAA,CADA,eACA,CAEA,kDAEI,WAAA,CADA,UACA,CAIR,wCACI,oBAAA,CAMZ,mBASI,uBAAA,CAAA,oBAAA,CACA,wBAAA,CAAA,qBAAA,CAAA,kBAAA,CAHA,+BAAA,CACA,YAAA,CAFA,WAAA,CAGA,sBAAA,CANA,MAAA,CAFA,cAAA,CAGA,KAAA,CACA,UAAA,CAHA,aAQA,CAGA,8CACI,eAAA,CAEA,iBAAA,CAGA,2CAAA,CAAA,mCAAA,CAGA,eAAA,CAJA,eAAA,CAHA,YAAA,CAKA,iBAAA,CACA,iBAAA,CAJA,SAKA,CAGA,8EACI,eAAA,CACA,iFAGI,UAAA,CADA,eAAA,CAEA,kBAAA,CAHA,YAGA,CAIJ,qFAKI,cAAA,CADA,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,QAGA,CAMJ,+EAKI,qBAAA,CACA,iBAAA,CACA,aAAA,CALA,YAAA,CACA,aAAA,CACA,YAAA,CAIA,eAAA,CAPA,UAOA,CAKR,8EAEI,uBAAA,CAAA,oBAAA,CACA,wBAAA,CAAA,qBAAA,CADA,sBAAA,CADA,mBAAA,CAAA,mBAAA,CAAA,YAAA,CAEA,6BAAA,CAEA,qFACI,wBAAA,CAEA,WAAA,CACA,iBAAA,CAFA,UAAA,CAKA,cAAA,CADA,aAAA,CAEA,UAAA,CAHA,iBAAA,CAIA,6DAAA,CAAA,qDAAA,CAAA,6CAAA,CAAA,mEAAA,CAEA,2FACI,wBAAA,CACA,6BAAA,CAAA,qBAAA,CAGJ,2FACI,YAAA,CAIJ,sGACI,sBAAA,CAEA,wBAAA,CADA,aACA,CAQhB,iCACI,2BAAA,CAGJ,gCACI,SAAA,CAMJ,wFAKI,wBAAA,CAAA,qBAAA,CAAA,kBAAA,CAHA,kBAAA,CAMA,iBAAA,CALA,UAAA,CACA,0BAAA,CAAA,0BAAA,CAAA,mBAAA,CAGA,QAAA,CAGA,kBAAA,CADA,eAAA,CAHA,YAAA,CALA,uBASA,CAEA,gGAEI,WAAA,CADA,UACA,CAIR,wEACI,aAAA,CAIA,wEACI,eAAA,CAKR,8EAGI,eAAA,CAFA,oBAAA,CACA,UACA,CAGJ,wKAEI,YAAA,CACA,eAAA,CAEA,oMAEI,SAAA,CADA,eACA,CAKZ,8BAEI,wBAAA,CAAA,qBAAA,CACA,wBAAA,CAAA,qBAAA,CAAA,kBAAA,CAFA,mBAAA,CAAA,mBAAA,CAAA,YAAA,CAGA,QAAA,CAFA,6BAAA,CAGA,cAAA,CAGJ,uBACI,cAAA,CACA,eAAA,CAEA,4BAGI,aAAA,CAFA,cAAA,CACA,eACA,CAKJ,2BACI,UAAA,CAIR,8BAGI,cAAA,CACA,eAAA,CAFA,gBAAA,CADA,iBAGA,CAGJ,iBAEI,WAAA,CADA,YAAA,CAKA,cAAA,CADA,UAAA,CAFA,eAAA,CACA,iBAEA","file":"mt-admin.css"}1 {"version":3,"sources":["mt-admin.scss"],"names":[],"mappings":"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","file":"mt-admin.css"} -
media-tracker/trunk/assets/src/scss/mt-admin.scss
r3432010 r3454648 4 4 */ 5 5 6 .mediatracker-usage-table { 7 margin-top: 20px; 8 border: 1px solid #ddd; 9 border-radius: 4px; 10 padding: 10px; 11 background-color: #fff; 12 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 13 14 table { 15 width: 100%; 16 border-collapse: collapse; 17 18 th, td { 19 padding: 10px; 20 text-align: left; 21 border-bottom: 1px solid #ddd; 22 text-transform: capitalize; 23 } 24 25 th { 26 background-color: #f4f4f4; 27 } 28 29 tr:nth-child(even) { 30 background-color: #f9f9f9; 31 } 32 33 tr:hover { 34 background-color: #f1f1f1; 35 } 36 37 a { 38 color: #0073aa; 39 text-decoration: none; 40 41 &:hover { 42 text-decoration: underline; 43 } 44 } 45 } 46 } 47 48 // Unused Media List CSS 49 .unused-media-list { 50 .wp-list-table { 51 td strong, 52 td strong { 53 display: block; 54 margin-bottom: .2em; 55 font-size: 14px; 56 } 57 58 .media-icon { 59 float: left; 60 min-height: 60px; 61 margin: 0 9px 0 0; 62 63 img { 64 width: 60px; 65 height: 60px; 66 } 67 } 68 69 &.fixed { 70 table-layout: inherit; 71 } 72 } 6 :root { 7 --primary: #6366f1; 8 --primary-dark: #4f46e5; 9 --bg: #f8fafc; 10 --card: #ffffff; 11 --text-main: #1e293b; 12 --text-muted: #64748b; 13 --border: #e2e8f0; 14 --success: #22c55e; 15 --danger: #ef4444; 16 --warning: #f59e0b; 73 17 } 74 18 … … 179 123 } 180 124 181 .wrap.unused-media-list, 182 .wrap.broken-link-checker { 183 .wp-heading-inline { 184 width: calc(100% - 40px); 185 background: #28a745; 125 .wrap { 126 &.unused-media-list, 127 &.broken-link-checker { 128 .wp-heading-inline { 129 width: calc(100% - 40px); 130 background: #28a745; 131 color: #fff; 132 display: inline-flex; 133 align-items: center; 134 padding: 20px 20px; 135 gap: 20px; 136 border-radius: 5px; 137 margin-top: 15px; 138 margin-bottom: 10px; 139 140 svg { 141 width: 40px; 142 height: 40px; 143 } 144 } 145 } 146 } 147 148 .media-tracker-layout { 149 margin: 0; 150 padding: 0; 151 box-sizing: border-box; 152 box-sizing: inherit; 153 background-color: var(--bg); 154 color: var(--text-main); 155 display: flex; 156 min-height: 100vh; 157 margin-top: 24px; 158 width: 98.8%; 159 160 h2 { 161 margin: 0; 162 padding: 0; 163 display: flex; 164 align-items: center; 165 gap: 6px; 166 } 167 168 aside { 169 width: 260px; 170 background: #0f172a; 171 color: white; 172 padding: 0; 173 display: flex; 174 flex-direction: column; 175 padding: 12px; 176 177 .version { 178 text-align: center; 179 padding: 15px; 180 } 181 } 182 183 .logo { 184 font-size: 1.2rem; 185 font-weight: bold; 186 margin: 10px 0px 20px; 187 display: flex; 188 align-items: center; 189 gap: 8px; 190 color: #ffffff; 191 } 192 193 nav { 194 flex: 1; 195 196 ul { 197 list-style: none; 198 margin: 0; 199 } 200 201 li { 202 padding: 13px 10px; 203 cursor: pointer; 204 transition: 0.25s; 205 display: flex; 206 align-items: center; 207 justify-content: flex-start; 208 gap: 10px; 209 font-size: 14px; 210 color: #ffffff; 211 letter-spacing: .1px; 212 margin: 0 0 3px; 213 border-radius: 4px; 214 215 &:hover, 216 &.active { 217 background: #6366f1; 218 color: #ffffff; 219 } 220 221 i { 222 width: 20px; 223 text-align: left; 224 } 225 } 226 } 227 228 ul { 229 li { 230 a { 231 color: #fff; 232 text-decoration: none; 233 width: 100%; 234 padding: 15px; 235 outline: none; 236 border: none; 237 display: flex; 238 align-items: center; 239 text-decoration: none; 240 gap: 10px; 241 242 small { 243 margin-left: auto; 244 display: flex; 245 align-items: center; 246 } 247 248 &:focus { 249 outline: none; 250 box-shadow: none; 251 } 252 } 253 254 &.license, 255 &.settings, 256 &.multisite { 257 padding: 15px !important; 258 } 259 260 &:last-child { 261 padding: 0; 262 } 263 264 &:hover { 265 a { 266 color: #ffffff; 267 } 268 } 269 } 270 } 271 272 main { 273 flex: 1; 274 padding: 2rem; 275 overflow-y: auto; 276 background: #ffffff; 277 } 278 279 header { 280 display: flex; 281 justify-content: space-between; 282 align-items: center; 283 margin-bottom: 2rem; 284 } 285 286 .status-badge { 287 background: #dcfce7; 288 color: #166534; 289 padding: 4px 12px; 290 border-radius: 20px; 291 font-size: 12px; 292 font-weight: 600; 293 } 294 295 h1 { 296 font-size: 22px; 297 font-weight: 600; 298 letter-spacing: -0.01em; 299 margin: 0; 300 } 301 302 .stats-grid { 303 display: grid; 304 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); 305 gap: 1.5rem; 306 margin-bottom: 1.5rem; 307 } 308 309 .card { 310 max-width: 100%; 311 background: #ffffff; 312 padding: 1.5rem; 313 border-radius: 10px; 314 border: 1px solid #e2e8f0; 315 box-shadow: 0 1px 3px rgba(0, 0, 0, .05); 316 margin: 0; 317 318 table { 319 .btn { 320 font-size: 12px; 321 width: 80px; 322 display: inline-flex; 323 } 324 } 325 326 h3 { 327 font-size: 16px; 328 color: #313335; 329 margin-bottom: 12px; 330 margin-top: 0; 331 332 i { 333 color: #6366f1; 334 } 335 } 336 337 .value { 338 font-size: 18px; 339 line-height: normal; 340 font-weight: bold; 341 display: block; 342 margin-bottom: 3px; 343 } 344 } 345 346 .progress-bar { 347 height: 8px; 348 background: #e5e7eb; 349 border-radius: 4px; 350 margin-top: 10px; 351 overflow: hidden; 352 } 353 354 .progress-fill { 355 height: 100%; 356 background: #6366f1; 357 border-radius: 4px; 358 } 359 360 .section-title { 361 font-size: 16px; 362 } 363 364 .grid-two { 365 display: grid; 366 grid-template-columns: 2.05fr 1fr; 367 gap: 1.5rem; 368 } 369 370 .grid-three { 371 display: grid; 372 grid-template-columns: 1fr 1fr 1fr; 373 gap: 1.5rem; 374 } 375 376 .setting-item { 377 display: flex; 378 justify-content: space-between; 379 align-items: center; 380 padding: 12px 0; 381 border-bottom: 1px solid #e2e8f0; 382 383 p { 384 margin: 0; 385 } 386 387 &:last-child { 388 border: none; 389 } 390 } 391 392 .switch { 393 position: relative; 394 display: inline-block; 395 width: 44px; 396 height: 22px; 397 398 input { 399 opacity: 0; 400 width: 0; 401 height: 0; 402 } 403 } 404 405 .slider { 406 position: absolute; 407 cursor: pointer; 408 top: 0; 409 left: 0; 410 right: 0; 411 bottom: 0; 412 background-color: #cbd5f5; 413 transition: .25s; 414 border-radius: 34px; 415 416 &:before { 417 position: absolute; 418 content: ""; 419 height: 16px; 420 width: 16px; 421 left: 3px; 422 bottom: 3px; 423 background-color: #fff; 424 transition: .25s; 425 border-radius: 50%; 426 } 427 } 428 429 input:checked + .slider { 430 background-color: #6366f1; 431 432 &:before { 433 transform: translateX(22px); 434 } 435 } 436 437 .btn { 438 padding: 12px 20px; 439 border-radius: 6px; 440 border: none; 441 font-weight: 600; 442 cursor: pointer; 443 transition: .2s; 444 font-size: 14px; 445 display: flex; 446 align-items: center; 447 justify-content: center; 448 gap: 5px; 449 text-decoration: none; 450 451 i { 452 font-size: 14px; 453 height: auto; 454 } 455 } 456 457 .btn-primary { 458 background: #6366f1; 186 459 color: #fff; 187 display: inline-flex; 188 align-items: center; 189 padding: 20px 20px; 190 gap: 20px; 191 border-radius: 5px; 460 461 &:hover { 462 background: #4f46e5; 463 } 464 } 465 466 .btn-outline { 467 background: transparent; 468 border: 1px solid #e2e8f0; 469 color: #1e293b; 470 471 &:hover { 472 background: #f1f5f9; 473 } 474 } 475 476 .btn-danger { 477 background: #ef4444; 478 color: #fff; 479 } 480 481 table { 482 width: 100%; 483 border-collapse: collapse; 484 margin-top: 1rem; 485 } 486 487 th { 488 text-align: left; 489 padding: 12px; 490 font-size: 14px; 491 color: #64748b; 492 493 &:last-child { 494 text-align: center; 495 } 496 } 497 498 td { 499 padding: 12px; 500 border-bottom: 1px solid #c3c4c7; 501 font-size: 14px; 502 } 503 504 tr:last-child { 505 td { 506 border-bottom: none; 507 } 508 } 509 510 .tag { 511 padding: 2px 8px; 512 border-radius: 4px; 513 font-size: 11px; 514 font-weight: bold; 515 text-transform: uppercase; 516 } 517 518 .tag-unused { 519 background: #fee2e2; 520 color: #991b1b; 521 } 522 523 .tag-duplicate { 524 background: #fef3c7; 525 color: #92400e; 526 } 527 528 .tab-content { 529 display: none; 530 531 &.active { 532 display: block; 533 } 534 } 535 536 .page-subtitle { 537 font-size: 13px; 538 color: #64748b; 539 margin-top: 10px; 540 margin-bottom: 0; 541 } 542 543 .stacked { 544 display: flex; 545 flex-direction: column; 546 gap: 10px; 547 margin-top: 10px; 548 } 549 550 .inline-actions { 551 display: flex; 552 gap: 10px; 553 align-items: center; 554 flex-wrap: wrap; 555 justify-content: flex-end; 556 } 557 558 .modal-backdrop { 559 position: fixed; 560 inset: 0; 561 background: rgba(15, 23, 42, .55); 562 display: none; 563 align-items: center; 564 justify-content: center; 565 z-index: 50; 566 567 &.active { 568 display: flex; 569 } 570 } 571 572 .modal { 573 background: #ffffff; 574 border-radius: 12px; 575 padding: 1.5rem; 576 width: 480px; 577 max-width: 94%; 578 box-shadow: 0 20px 40px rgba(15, 23, 42, .3); 579 border: 1px solid #e2e8f0; 580 } 581 582 .modal-header { 583 display: flex; 584 align-items: center; 585 justify-content: space-between; 586 margin-bottom: 1rem; 587 } 588 589 .modal-title { 590 font-size: 18px; 591 font-weight: 600; 592 display: flex; 593 align-items: center; 594 gap: 8px; 595 } 596 597 .modal-close { 598 border: none; 599 background: transparent; 600 cursor: pointer; 601 font-size: 18px; 602 color: #64748b; 603 } 604 605 .modal-body { 606 display: flex; 607 flex-direction: column; 608 gap: 12px; 609 margin-bottom: 1.5rem; 610 611 label { 612 font-size: 13px; 613 margin-bottom: 4px; 614 display: block; 615 } 616 617 input, select { 618 width: 100%; 619 padding: 8px 10px; 620 border-radius: 6px; 621 border: 1px solid #e2e8f0; 622 font-size: 13px; 623 624 &:focus { 625 outline: none; 626 border-color: #6366f1; 627 box-shadow: 0 0 0 1px rgba(99, 102, 241, .25); 628 } 629 } 630 } 631 632 .modal-hint { 633 font-size: 11px; 634 color: #64748b; 635 } 636 637 .modal-footer { 638 display: flex; 639 justify-content: flex-end; 640 gap: 10px; 641 } 642 643 /* Nested Components from top-level */ 644 645 .mediatracker-usage-table { 192 646 margin-top: 15px; 193 margin-bottom: 10px; 194 195 svg { 196 width: 40px; 197 height: 40px; 198 } 199 } 200 201 .wp-filter { 202 margin: 10px 0 10px; 203 } 204 205 .notice { 647 648 table.wp-list-table { 649 th, td { 650 vertical-align: middle; 651 } 652 653 .button { 654 margin-right: 4px; 655 } 656 } 657 } 658 659 // Unused Media List CSS 660 .unused-media-list { 661 .wp-list-table { 662 td strong { 663 display: block; 664 margin-bottom: .2em; 665 font-size: 14px; 666 } 667 668 .media-icon { 669 float: left; 670 min-height: 60px; 671 margin: 0 9px 0 0; 672 673 img { 674 width: 60px; 675 height: 60px; 676 } 677 } 678 679 &.fixed { 680 table-layout: inherit; 681 } 682 } 683 684 .search-box { 685 margin: 0px; 686 } 687 688 .wp-filter { 689 margin: 0 0 20px; 690 } 691 692 .notice { 693 h2 { 694 margin-bottom: 0; 695 } 696 } 697 698 /* Simple styling for the admin table */ 699 table.widefat { 700 table-layout: inherit; 701 width: 100%; 702 margin-top: 20px; 703 704 th, 705 td { 706 padding: 10px; 707 text-align: left; 708 709 &.status { 710 font-weight: 700; 711 color: red; 712 } 713 } 714 } 715 } 716 717 .media-toolbar-wrap { 718 &.wp-filter { 719 display: flex; 720 justify-content: space-between; 721 align-items: center; 722 gap: 20px; 723 border: none; 724 box-shadow: none; 725 726 .search-form { 727 input[type=search] { 728 width: 215px; 729 } 730 } 731 } 732 } 733 734 .unused-image-found { 206 735 h2 { 207 margin-bottom: 0; 208 } 209 } 210 211 /* Simple styling for the admin table */ 212 table.widefat { 213 table-layout: inherit; 736 font-size: 20px; 737 font-weight: 400; 738 739 span { 740 font-size: 20px; 741 font-weight: bold; 742 color: #6366f1; 743 } 744 } 745 } 746 747 .replace-broken-link { 748 input { 749 width: 100%; 750 } 751 } 752 753 #clear-broken-links-transient { 754 position: absolute; 755 padding: 8px 30px; 756 font-size: 14px; 757 font-weight: 500; 758 } 759 760 #success-message { 761 display: none; 762 color: green; 763 margin-top: 15px; 764 position: absolute; 765 left: 230px; 766 font-size: 16px; 767 } 768 769 .wp-list-table { 770 #usage_count { 771 width: 130px; 772 } 773 } 774 775 // Duplicate Form & Components 776 #mt-duplicate-form { 777 table { 778 tbody td:last-child, 779 tr th:last-child { 780 text-align: center; 781 } 782 783 .check-column { 784 background: transparent; 785 border-bottom: 1px solid #c3c4c7; 786 width: 3.2em; 787 padding: 16px 3px; 788 } 789 } 790 791 .duplicate-media-footer { 792 display: flex; 793 align-items: center; 794 gap: 30px; 795 justify-content: space-between; 796 margin-top: 15px; 797 } 798 799 .tablenav-pages { 800 .paging-input { 801 margin: 0 15px; 802 } 803 } 804 } 805 806 .duplicate-media-count { 807 h2 { 808 font-size: 20px; 809 color: #6366f1; 810 font-weight: 700; 811 812 span { 813 color: #1d2327; 814 font-weight: 400; 815 } 816 } 817 } 818 819 .media-header { 820 display: flex; 821 justify-content: space-between; 822 align-items: center; 823 margin-bottom: 30px; 824 } 825 826 #tab-unused-media { 827 .tablenav.bottom { 828 margin-top: 15px; 829 } 830 } 831 832 .mt-overview-table { 833 border-radius: 8px; 834 overflow: hidden; 835 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 836 margin-top: 20px; 214 837 width: 100%; 215 margin-top: 20px; 216 } 217 218 table.widefat th, 219 table.widefat td { 220 padding: 10px; 221 text-align: left; 222 223 &.status { 224 font-weight: 700; 225 color: red; 226 } 227 } 228 } 229 230 .media-toolbar-wrap.wp-filter { 838 border-collapse: separate; 839 border-spacing: 0; 840 background: #fff; 841 842 thead { 843 th { 844 background-color: #f8fafc; 845 color: #475569; 846 font-weight: 600; 847 text-transform: uppercase; 848 font-size: 11px; 849 letter-spacing: 0.05em; 850 padding: 16px; 851 border-bottom: 1px solid #e2e8f0; 852 text-align: left; 853 } 854 855 tr:first-child { 856 th:first-child { border-top-left-radius: 8px; } 857 th:last-child { border-top-right-radius: 8px; } 858 } 859 } 860 861 tbody { 862 tr { 863 transition: background-color 0.2s; 864 865 &:nth-child(even) { 866 background-color: #f8fafc; 867 } 868 869 &:hover { 870 background-color: #f1f5f9; 871 } 872 873 &:last-child td { 874 border-bottom: none; 875 } 876 } 877 878 td { 879 padding: 16px; 880 color: #334155; 881 vertical-align: middle; 882 border-bottom: 1px solid #f1f5f9; 883 font-size: 14px; 884 885 &:last-child { 886 text-align: center; 887 } 888 889 strong { 890 color: #0f172a; 891 font-weight: 500; 892 } 893 894 small { 895 color: #94a3b8; 896 display: block; 897 margin-top: 4px; 898 } 899 } 900 901 tr:last-child { 902 td:first-child { border-bottom-left-radius: 8px; } 903 td:last-child { border-bottom-right-radius: 8px; } 904 } 905 } 906 } 907 908 // Helper Utility Classes 909 .mt-flex-col-end { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; } 910 .mt-flex-center { display: flex; gap: 10px; align-items: center; } 911 .mt-stat-title { display: flex; align-items: center; gap: 8px; } 912 .mt-icon-indigo { color: #6366f1; } 913 .mt-icon-amber { color: #f59e0b; } 914 .mt-stat-subtitle { color: #64748b; font-size: 12px; } 915 .mt-mt-10 { margin-top: 10px; } 916 .mt-helper-text { font-size: 12px; color: #64748b; } 917 .mt-btn-sm { padding: 6px 12px !important; font-size: 12px !important; } 918 .mt-btn-xs { padding: 5px 10px !important; } 919 .mt-mime-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8fafc; border-radius: 8px; } 920 .mt-mime-icon { color: #6366f1; width: 20px; text-align: center; } 921 .mt-flex-1 { flex: 1; } 922 .mt-font-medium { font-size: 14px; font-weight: 500; } 923 .mt-empty-state { color: #64748b; font-size: 12px; text-align: center; padding: 20px; } 924 .mt-mt-6 { margin-top: 1.5rem; } 925 .mt-empty-state-large { color: #64748b; text-align: center; padding: 40px; } 926 .mt-success-icon-large { font-size: 48px; color: #10b981; margin-bottom: 15px; height: 48px; width: 48px; } 927 .mt-tag-success { background: #10b981; color: white; } 928 .mt-btn-xs-clean { padding: 5px 10px !important; text-decoration: none; } 929 .mt-chart-icon-large { font-size: 48px; color: #cbd5e1; margin-bottom: 15px; height: 48px; width: 48px; } 930 .mt-mb-3 { margin-bottom: 12px; } 931 .mt-progress-text { margin-left: 8px; font-size: 11px; color: #64748b; min-width: 40px; text-align: right; display: none; } 932 .mt-status-text { margin-left: 4px; display: none; } 933 .mt-progress-track { flex: 1; max-width: 100%; height: 15px; background: linear-gradient(90deg, #eef2ff, #e2e8f0); border-radius: 999px; overflow: hidden; display: none; box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.4); } 934 .mt-progress-bar-fill { width: 0%; height: 100%; background: #6366f1; } 935 .mt-v-middle { vertical-align: middle; } 936 .mt-text-center { text-align: center; } 937 .mt-mr-1 { margin-right: 4px; } 938 .mt-thumb-img { width: 60px; height: auto; } 939 .mt-link-clean { text-decoration: none; font-weight: 500; } 940 941 @media (max-width: 960px) { 942 flex-direction: column; 943 944 aside { 945 width: 100%; 946 flex-direction: row; 947 align-items: center; 948 justify-content: space-between; 949 padding: 1rem 1.5rem; 950 } 951 952 nav ul { 953 display: flex; 954 overflow-x: auto; 955 } 956 957 nav li { 958 white-space: nowrap; 959 } 960 } 961 } 962 963 /* Featured Video Card */ 964 .media-video-featured-card { 965 background: #fff; 966 border: 1px solid #e2e8f0; 967 border-radius: 12px; 968 overflow: hidden; 969 display: flex; 970 flex-direction: column; /* Side by side layout */ 971 align-items: stretch; 972 transition: all 0.3s ease; 973 cursor: pointer; 974 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, .05); 975 width: 32%; 976 977 &:hover { 978 transform: translateY(-2px); 979 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 980 border-color: #cbd5e1; 981 982 .video-thumbnail { 983 opacity: 0.6; 984 } 985 986 .play-icon { 987 transform: scale(1.1); 988 background: #fff; 989 box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.3); 990 } 991 } 992 993 .video-thumbnail-wrapper { 994 position: relative; 995 background: #000; 996 overflow: hidden; 997 display: flex; 998 align-items: center; 999 justify-content: center; 1000 } 1001 1002 .video-thumbnail { 1003 width: 100%; 1004 height: 100%; 1005 object-fit: cover; 1006 opacity: 0.8; 1007 transition: opacity 0.3s ease; 1008 } 1009 1010 .play-button-overlay { 1011 position: absolute; 1012 top: 0; 1013 left: 0; 1014 width: 100%; 1015 height: 100%; 1016 display: flex; 1017 align-items: center; 1018 justify-content: center; 1019 z-index: 2; 1020 } 1021 1022 .play-icon { 1023 width: 60px; 1024 height: 60px; 1025 background: rgba(255, 255, 255, 0.9); 1026 border-radius: 50%; 1027 display: flex; 1028 align-items: center; 1029 justify-content: center; 1030 transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); 1031 box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); 1032 1033 .dashicons { 1034 font-size: 32px; 1035 width: 32px; 1036 height: 32px; 1037 color: #6366f1; /* Primary color */ 1038 margin-left: 4px; /* Visual adjustment */ 1039 } 1040 } 1041 1042 .video-card-content { 1043 padding: 2rem; 1044 flex: 1; 1045 display: flex; 1046 flex-direction: column; 1047 justify-content: center; 1048 align-items: flex-start; 1049 1050 h4 { 1051 margin: 0 0 0.75rem 0; 1052 font-size: 1.25rem; 1053 color: #1e293b; 1054 font-weight: 600; 1055 } 1056 } 1057 } 1058 1059 /* Modal Styles */ 1060 .mt-video-modal-backdrop { 1061 position: fixed; 1062 inset: 0; 1063 background: rgba(15, 23, 42, 0.75); 1064 z-index: 100000; /* High z-index */ 1065 display: none; 1066 align-items: center; 1067 justify-content: center; 1068 backdrop-filter: blur(4px); 1069 opacity: 0; 1070 transition: opacity 0.3s ease; 1071 1072 &.active { 1073 display: flex; 1074 opacity: 1; 1075 1076 .mt-video-modal { 1077 transform: scale(1); 1078 } 1079 } 1080 } 1081 1082 .mt-video-modal { 1083 background: #fff; 1084 width: 90%; 1085 max-width: 800px; 1086 border-radius: 16px; 1087 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); 1088 overflow: hidden; 1089 transform: scale(0.95); 1090 transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 1091 } 1092 1093 .mt-video-modal-header { 1094 padding: 1rem 1.5rem; 1095 border-bottom: 1px solid #e2e8f0; 231 1096 display: flex; 232 1097 justify-content: space-between; 233 1098 align-items: center; 234 gap: 20px; 235 padding: 0 15px; 236 237 .search-form { 238 input[type=search] { 239 width: 215px; 240 } 241 } 242 } 243 244 .unused-image-found h2 { 245 font-size: 24px; 246 font-weight: 300; 247 248 span { 249 font-size: 28px; 250 font-weight: bold; 251 color: #cf0000; 252 } 253 } 254 255 .replace-broken-link { 256 input { 1099 background: #f8fafc; 1100 1101 h3 { 1102 margin: 0; 1103 font-size: 1.1rem; 1104 color: #334155; 1105 } 1106 } 1107 1108 .mt-video-modal-close { 1109 background: transparent; 1110 border: none; 1111 cursor: pointer; 1112 color: #64748b; 1113 padding: 4px; 1114 border-radius: 4px; 1115 display: flex; 1116 align-items: center; 1117 justify-content: center; 1118 transition: background 0.2s; 1119 1120 &:hover { 1121 background: #e2e8f0; 1122 color: #ef4444; 1123 } 1124 } 1125 1126 .mt-video-modal-body { 1127 padding: 0; 1128 background: #000; 1129 } 1130 1131 .mt-responsive-video-wrapper { 1132 position: relative; 1133 padding-bottom: 56.25%; /* 16:9 Aspect Ratio */ 1134 height: 0; 1135 overflow: hidden; 1136 1137 iframe { 1138 position: absolute; 1139 top: 0; 1140 left: 0; 257 1141 width: 100%; 258 } 259 } 260 261 #clear-broken-links-transient { 262 position: absolute; 263 padding: 8px 30px; 264 font-size: 14px; 265 font-weight: 500; 266 } 267 268 #success-message { 269 display: none; 270 color: green; 271 margin-top: 15px; 272 position: absolute; 273 left: 230px; 274 font-size: 16px; 275 } 276 277 .wp-list-table { 278 #usage_count { 279 width: 130px; 280 } 281 } 1142 height: 100%; 1143 border: 0; 1144 } 1145 } 1146 1147 /* Responsive adjustments */ 1148 @media (max-width: 768px) { 1149 .media-video-featured-card { 1150 flex-direction: column; 1151 1152 .video-thumbnail-wrapper { 1153 width: 100%; 1154 height: 200px; 1155 } 1156 1157 .video-card-content { 1158 padding: 1.5rem; 1159 } 1160 } 1161 } -
media-tracker/trunk/composer.json
r3151282 r3454648 15 15 "Media_Tracker\\": "includes/" 16 16 }, 17 "files": [] 17 "files": [ 18 "includes/functions.php" 19 ] 18 20 } 19 21 } -
media-tracker/trunk/includes/Admin.php
r3432010 r3454648 17 17 new Admin\Media_Usage(); 18 18 new Admin\Duplicate_Images(); 19 new Admin\PluginMeta(); 19 20 } 20 21 } -
media-tracker/trunk/includes/Admin/Duplicate_Images.php
r3432010 r3454648 24 24 add_action('wp_ajax_get_duplicate_images', array($this, 'get_duplicate_images_via_ajax')); 25 25 add_action('wp_ajax_reset_duplicate_hashes', array($this, 'reset_duplicate_hashes_via_ajax')); 26 27 // Add faster cron schedule (5 minutes) for quicker initial hashing 28 add_filter('cron_schedules', function($schedules) { 29 if (!isset($schedules['five_minutes'])) { 30 $schedules['five_minutes'] = array( 31 'interval' => 5 * 60, 32 'display' => __('Every 5 Minutes', 'media-tracker'), 33 ); 34 } 35 return $schedules; 36 }); 37 38 // Schedule the cron job for batch processing 39 if (!wp_next_scheduled('media_tracker_batch_process')) { 40 // Prefer the faster schedule when available 41 $interval = 'five_minutes'; 42 $schedules = wp_get_schedules(); 43 if (!isset($schedules[$interval])) { 44 $interval = 'hourly'; 45 } 46 wp_schedule_event(time(), $interval, 'media_tracker_batch_process'); 47 } 26 add_action('wp_ajax_mt_delete_duplicate_images', array($this, 'delete_duplicate_images_via_ajax')); 48 27 } 49 28 … … 94 73 95 74 // If no duplicates yet and many attachments lack hashes, trigger a quick batch 75 // Manual scan only: disable auto-trigger 76 /* 96 77 if (empty($hashes) && $this->count_unhashed_attachments() > 0) { 97 78 // Run one batch immediately to bootstrap hashes; then re-check … … 99 80 $hashes = $this->get_duplicate_hashes_by_sql(); 100 81 } 82 */ 101 83 102 84 $duplicate_ids = array(); … … 358 340 */ 359 341 public function process_image_hashes_batch() { 342 // Manual scan only: check if scan is active 343 if ( ! get_option( 'media_tracker_duplicate_scan_active' ) ) { 344 return; 345 } 346 360 347 $limit = MEDIA_TRACKER_DUPLICATE_BATCH_SIZE; 361 348 $offset = get_option('media_tracker_offset', 0); … … 367 354 // If no more images, reset the offset 368 355 delete_option('media_tracker_offset'); 356 // Scan complete: disable active flag 357 delete_option( 'media_tracker_duplicate_scan_active' ); 358 359 // Save final duplicate count 360 $count = self::count_duplicate_attachments(); 361 update_option( 'media_tracker_duplicate_count_last_scan', $count ); 362 363 // Invalidate dashboard stats cache 364 delete_transient( 'media_tracker_dashboard_stats_v8' ); 369 365 } else { 370 366 // Update the offset for the next batch … … 386 382 $hashes = $this->get_duplicate_hashes_by_sql(); 387 383 // If no duplicates yet and many attachments lack hashes, trigger a quick batch 384 // Manual scan only: disable auto-trigger 385 /* 388 386 if (empty($hashes) && $this->count_unhashed_attachments() > 0) { 389 387 do_action('media_tracker_batch_process'); 390 388 $hashes = $this->get_duplicate_hashes_by_sql(); 391 389 } 390 */ 392 391 $duplicate_ids = array(); 393 392 $ids_with_hashes = array(); … … 434 433 wp_send_json_error(); 435 434 } 435 } 436 437 /** 438 * Get the total count of duplicate images. 439 * 440 * @return int Total number of duplicate images. 441 */ 442 public static function count_duplicate_attachments() { 443 global $wpdb; 444 445 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 446 $wpdb->prepare( 447 "SELECT COUNT(*) as count 448 FROM {$wpdb->postmeta} pm 449 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id 450 WHERE pm.meta_key = %s 451 AND p.post_type = 'attachment' 452 AND p.post_mime_type LIKE %s 453 AND pm.meta_value != '' 454 GROUP BY pm.meta_value 455 HAVING count > 1", 456 '_media_tracker_hash', 457 'image/%' 458 ) 459 ); 460 461 $count = 0; 462 if ( $results ) { 463 foreach ( $results as $row ) { 464 $count += $row->count; 465 } 466 } 467 468 return $count; 436 469 } 437 470 … … 517 550 // Add a new helper that aggregates hashes across the entire library 518 551 private function get_all_duplicate_hashes($batch_size = 300) { 519 global $wpdb; 520 521 $offset = 0; 522 $hashes = array(); 523 $iterations = 0; 524 $max_iterations = 10000; // Safety limit 525 526 while ($iterations < $max_iterations) { 527 $attachments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 528 $wpdb->prepare( 529 "SELECT ID, guid FROM {$wpdb->posts} 530 WHERE post_type = %s AND post_mime_type LIKE %s 531 LIMIT %d, %d", 532 'attachment', 533 'image/%', 534 $offset, 535 $batch_size 536 ) 537 ); 538 539 if (empty($attachments)) { 540 break; 541 } 542 543 foreach ($attachments as $attachment) { 544 $file_path = get_attached_file($attachment->ID); 545 546 if ( $file_path && file_exists($file_path) ) { 547 $hash = get_post_meta($attachment->ID, '_media_tracker_hash', true); 548 549 // Get mime for deciding hashing strategy 550 $mime = get_post_mime_type($attachment->ID); 551 552 if (empty($hash)) { 553 try { 554 $hash = $this->generate_image_hash($file_path, $mime); 555 update_post_meta($attachment->ID, '_media_tracker_hash', $hash); 556 } catch (Exception $e) { 557 // Silently skip images that can't be hashed 558 continue; 559 } 560 } 561 562 if (!isset($hashes[$hash])) { 563 $hashes[$hash] = array(); 564 } 565 $hashes[$hash][] = $attachment->ID; 566 } 567 } 568 569 $offset += $batch_size; 570 $iterations++; 571 572 if (count($attachments) < $batch_size) { 573 break; 574 } 575 } 576 577 // Only return hashes with duplicates across the entire set 578 return array_filter($hashes, function($ids) { 579 return count($ids) > 1; 580 }); 552 // Strictly use existing database hashes. 553 // Never generate new hashes here. Hash generation is now exclusively handled 554 // by the manual scan process (reset_duplicate_hashes_via_ajax -> batch process). 555 return $this->get_duplicate_hashes_by_sql(); 581 556 } 582 557 … … 604 579 // Reset the offset 605 580 delete_option('media_tracker_offset'); 581 582 // Activate manual scan flag 583 update_option( 'media_tracker_duplicate_scan_active', true ); 584 585 // Mark that a scan has been initiated/performed 586 update_option( 'media_tracker_duplicates_scanned', true ); 587 588 // Clear dashboard stats cache so overview updates 589 delete_transient( 'media_tracker_dashboard_stats_v8' ); 606 590 607 591 // Trigger immediate batch processing … … 622 606 )); 623 607 } 608 609 public function delete_duplicate_images_via_ajax() { 610 if ( ! current_user_can( 'manage_options' ) ) { 611 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 ); 612 } 613 614 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 615 616 if ( empty( $_POST['attachment_ids'] ) || ! is_array( $_POST['attachment_ids'] ) ) { 617 wp_send_json_error( array( 'message' => __( 'No images selected.', 'media-tracker' ) ) ); 618 } 619 620 $ids = array_map( 'intval', $_POST['attachment_ids'] ); 621 $deleted = 0; 622 623 foreach ( $ids as $id ) { 624 if ( $id <= 0 ) { 625 continue; 626 } 627 if ( ! current_user_can( 'delete_post', $id ) ) { 628 continue; 629 } 630 $result = wp_delete_attachment( $id, true ); 631 if ( $result ) { 632 $deleted++; 633 } 634 } 635 636 if ( $deleted > 0 ) { 637 // Clear dashboard stats cache so overview updates 638 delete_transient( 'media_tracker_dashboard_stats_v6' ); 639 640 wp_send_json_success( array( 641 'message' => sprintf( 642 /* translators: %d: number of deleted images */ 643 __( 'Deleted %d duplicate images.', 'media-tracker' ), 644 $deleted 645 ), 646 'deleted' => $deleted, 647 ) ); 648 } else { 649 wp_send_json_error( array( 'message' => __( 'No images were deleted.', 'media-tracker' ) ) ); 650 } 651 } 652 653 /** 654 * Check if any media tracker hash exists in the database. 655 * 656 * @return bool True if hash exists, false otherwise. 657 */ 658 public static function has_generated_hashes() { 659 global $wpdb; 660 $hash_exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 661 $wpdb->prepare( 662 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT 1", 663 '_media_tracker_hash' 664 ) 665 ); 666 return ! empty( $hash_exists ); 667 } 668 669 /** 670 * Render pagination for duplicate images tab. 671 * 672 * @param int $current_page Current page number. 673 * @param int $total_pages Total number of pages. 674 * @param int $total_items Total number of items. 675 */ 676 public static function render_pagination( $current_page, $total_pages, $total_items ) { 677 if ( $total_pages <= 1 ) { 678 return; 679 } 680 681 $base_url = add_query_arg( 'tab', 'duplicates', remove_query_arg( array( 'mt_dup_page' ) ) ); 682 683 echo '<div class="tablenav"><div class="tablenav-pages">'; 684 echo '<span class="displaying-num">' . esc_html( $total_items ) . ' ' . esc_html__( 'items', 'media-tracker' ) . '</span>'; 685 686 if ( $current_page > 1 ) { 687 $prev_url = add_query_arg( 'mt_dup_page', $current_page - 1, $base_url ); 688 echo '<a class="prev-page button" href="' . esc_url( $prev_url ) . '">«</a>'; 689 } else { 690 echo '<span class="tablenav-pages-navspan">«</span>'; 691 } 692 693 echo '<span class="paging-input">' . esc_html( $current_page ) . ' / ' . esc_html( $total_pages ) . '</span>'; 694 695 if ( $current_page < $total_pages ) { 696 $next_url = add_query_arg( 'mt_dup_page', $current_page + 1, $base_url ); 697 echo '<a class="next-page button" href="' . esc_url( $next_url ) . '">»</a>'; 698 } else { 699 echo '<span class="tablenav-pages-navspan">»</span>'; 700 } 701 702 echo '</div></div>'; 703 } 624 704 } -
media-tracker/trunk/includes/Admin/Media_Usage.php
r3432010 r3454648 27 27 28 28 /** 29 * Get most used media for dashboard stats. 30 * 31 * @return array Array of objects with ID, post_title, post_mime_type, usage_count. 32 */ 33 public static function get_dashboard_most_used_media() { 34 global $wpdb; 35 36 $media_tracker_most_used = array(); 37 $media_tracker_all_usage = array(); 38 39 // Source 1: Featured images (_thumbnail_id). 40 $media_tracker_featured_media = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 41 "SELECT p.ID, p.post_title, p.post_mime_type, COUNT(pm.meta_value) as usage_count 42 FROM {$wpdb->posts} p 43 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.meta_value 44 WHERE pm.meta_key = '_thumbnail_id' 45 AND p.post_type = 'attachment' 46 AND p.post_status = 'inherit' 47 GROUP BY p.ID" 48 ); 49 50 foreach ( $media_tracker_featured_media as $media_tracker_item ) { 51 $media_tracker_id = $media_tracker_item->ID; 52 if ( ! isset( $media_tracker_all_usage[ $media_tracker_id ] ) ) { 53 $media_tracker_all_usage[ $media_tracker_id ] = array( 54 'ID' => $media_tracker_id, 55 'post_title' => $media_tracker_item->post_title, 56 'post_mime_type' => $media_tracker_item->post_mime_type, 57 'usage_count' => 0, 58 ); 59 } 60 $media_tracker_all_usage[ $media_tracker_id ]['usage_count'] += $media_tracker_item->usage_count; 61 } 62 63 // Source 2: Content usage - count ALL occurrences in post content. 64 $media_tracker_all_attachments = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 65 "SELECT ID FROM {$wpdb->posts} 66 WHERE post_type = 'attachment' 67 AND post_status = 'inherit'" 68 ); 69 70 foreach ( $media_tracker_all_attachments as $media_tracker_attachment_id ) { 71 // Count in post content (wp-image-XXXX class). 72 $media_tracker_wp_image_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 73 $wpdb->prepare( 74 "SELECT COUNT(DISTINCT ID) 75 FROM {$wpdb->posts} 76 WHERE post_status = 'publish' 77 AND post_content LIKE %s", 78 '%wp-image-' . intval( $media_tracker_attachment_id ) . '%' 79 ) 80 ); 81 82 // Count in post content (Gutenberg blocks). 83 $media_tracker_gutenberg_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 84 $wpdb->prepare( 85 "SELECT COUNT(DISTINCT ID) 86 FROM {$wpdb->posts} 87 WHERE post_status = 'publish' 88 AND post_content LIKE %s", 89 '%"id":' . intval( $media_tracker_attachment_id ) . ',%' 90 ) 91 ); 92 93 // Count in post content (direct URLs). 94 $media_tracker_attachment_url = wp_get_attachment_url( $media_tracker_attachment_id ); 95 $media_tracker_url_count = 0; 96 if ( $media_tracker_attachment_url ) { 97 $media_tracker_url_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 98 $wpdb->prepare( 99 "SELECT COUNT(DISTINCT ID) 100 FROM {$wpdb->posts} 101 WHERE post_status = 'publish' 102 AND post_content LIKE %s", 103 '%' . $wpdb->esc_like( $media_tracker_attachment_url ) . '%' 104 ) 105 ); 106 } 107 108 $media_tracker_total_usage = intval( $media_tracker_wp_image_count ) + intval( $media_tracker_gutenberg_count ) + intval( $media_tracker_url_count ); 109 110 if ( $media_tracker_total_usage > 0 ) { 111 if ( ! isset( $media_tracker_all_usage[ $media_tracker_attachment_id ] ) ) { 112 $media_tracker_post = get_post( $media_tracker_attachment_id ); 113 $media_tracker_all_usage[ $media_tracker_attachment_id ] = array( 114 'ID' => $media_tracker_attachment_id, 115 'post_title' => $media_tracker_post->post_title, 116 'post_mime_type' => $media_tracker_post->post_mime_type, 117 'usage_count' => 0, 118 ); 119 } 120 $media_tracker_all_usage[ $media_tracker_attachment_id ]['usage_count'] += $media_tracker_total_usage; 121 } 122 } 123 124 // Convert to object array and sort by usage. 125 foreach ( $media_tracker_all_usage as $media_tracker_usage ) { 126 $media_tracker_obj = new \stdClass(); 127 $media_tracker_obj->ID = $media_tracker_usage['ID']; 128 $media_tracker_obj->post_title = $media_tracker_usage['post_title']; 129 $media_tracker_obj->post_mime_type = $media_tracker_usage['post_mime_type']; 130 $media_tracker_obj->usage_count = $media_tracker_usage['usage_count']; 131 $media_tracker_most_used[] = $media_tracker_obj; 132 } 133 134 // Sort by usage count descending. 135 usort( 136 $media_tracker_most_used, 137 function( $a, $b ) { 138 return $b->usage_count - $a->usage_count; 139 } 140 ); 141 142 // Get top 5. 143 return array_slice( $media_tracker_most_used, 0, 5 ); 144 } 145 146 /** 147 * Get media type statistics for dashboard. 148 * 149 * @return array Array of objects with post_mime_type and count. 150 */ 151 public static function get_mime_type_stats() { 152 global $wpdb; 153 154 return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 155 $wpdb->prepare( 156 "SELECT post_mime_type, COUNT(*) as count 157 FROM {$wpdb->posts} 158 WHERE post_type = %s 159 AND post_status = %s 160 AND post_mime_type != '' 161 GROUP BY post_mime_type 162 ORDER BY count DESC 163 LIMIT 5", 164 'attachment', 165 'inherit' 166 ) 167 ); 168 } 169 170 /** 29 171 * Add a custom meta box to the media file details page 30 172 * … … 167 309 if ( $results ) { 168 310 echo ' 169 <table >311 <table class="wp-list-table widefat fixed striped table-view-list"> 170 312 <thead> 171 313 <tr> 172 <th >' . esc_html__( '#', 'media-tracker' ) . '</th>173 <th >' . esc_html__( 'Title', 'media-tracker' ) . '</th>174 <th >' . esc_html__( 'Type', 'media-tracker' ) . '</th>175 <th >' . esc_html__( 'Date Added', 'media-tracker' ) . '</th>176 <th >' . esc_html__( 'Actions', 'media-tracker' ) . '</th>314 <th scope="col" class="manage-column column-primary" style="width: 50px;">' . esc_html__( '#', 'media-tracker' ) . '</th> 315 <th scope="col" class="manage-column">' . esc_html__( 'Title', 'media-tracker' ) . '</th> 316 <th scope="col" class="manage-column">' . esc_html__( 'Type', 'media-tracker' ) . '</th> 317 <th scope="col" class="manage-column">' . esc_html__( 'Date Added', 'media-tracker' ) . '</th> 318 <th scope="col" class="manage-column">' . esc_html__( 'Actions', 'media-tracker' ) . '</th> 177 319 </tr> 178 320 </thead> … … 278 420 279 421 // Posts using this attachment as featured image 280 $thumbnail_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 422 $thumbnail_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 281 423 $wpdb->prepare( 282 424 " … … 295 437 // ACF usage 296 438 if(class_exists('ACF')) { 297 $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 439 $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 298 440 $wpdb->prepare( 299 441 " … … 319 461 $results = array_merge($results, $acf_posts); 320 462 321 $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 463 $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 322 464 $wpdb->prepare( 323 465 " … … 341 483 342 484 // Elementor usage 343 $elementor_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 485 $elementor_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 344 486 $wpdb->prepare( 345 487 " -
media-tracker/trunk/includes/Admin/Menu.php
r3432010 r3454648 18 18 */ 19 19 public function __construct() { 20 add_action( 'admin_menu', array( $this, 'register_unused_media_cleaner_menu' ) ); 21 add_filter( 'set-screen-option', array( $this, 'set_screen_option' ), 10, 3 ); 22 add_action( 'load-media_page_unused-media-cleaner', array( $this, 'add_screen_options' ) ); 23 // Suppress admin notices on our screen before header renders 24 add_action( 'load-media_page_unused-media-cleaner', array( $this, 'suppress_admin_notices' ) ); 20 add_action( 'admin_menu', array( $this, 'register_media_tracker_menu' ) ); 25 21 26 22 // Handle AJAX: clear plugin-related transients/cache … … 32 28 // Handle AJAX: run media scan in background (start) 33 29 add_action( 'wp_ajax_run_media_scan', array( $this, 'handle_run_media_scan' ) ); 30 34 31 // NEW: Handle AJAX: run media scan synchronously (fallback if cron stalls) 35 32 add_action( 'wp_ajax_run_media_scan_sync', array( $this, 'handle_run_media_scan_sync' ) ); … … 43 40 // Handle AJAX: get unused media count 44 41 add_action( 'wp_ajax_get_unused_media_count', array( $this, 'handle_get_unused_media_count' ) ); 42 43 // Handle AJAX: remove all unused media 44 add_action( 'wp_ajax_remove_all_unused_media', array( $this, 'handle_remove_all_unused_media' ) ); 45 46 // Hook into option updates to clear cache when site icon or theme mods change 47 add_action( 'updated_option', array( $this, 'handle_option_update' ), 10, 3 ); 48 } 49 50 /** 51 * Clear unused media cache when site icon or theme settings change. 52 * 53 * @param string $option Option name. 54 * @param mixed $old_value Old option value. 55 * @param mixed $new_value New option value. 56 */ 57 public function handle_option_update( $option, $old_value, $new_value ) { 58 $ids_to_remove = []; 59 60 if ( 'site_icon' === $option && ! empty( $new_value ) ) { 61 $ids_to_remove[] = intval( $new_value ); 62 } elseif ( 0 === strpos( $option, 'theme_mods_' ) ) { 63 // Theme mods updated 64 $mods = $new_value; 65 if ( is_array( $mods ) ) { 66 if ( ! empty( $mods['custom_logo'] ) ) { 67 $ids_to_remove[] = intval( $mods['custom_logo'] ); 68 } 69 if ( ! empty( $mods['background_image_thumb'] ) ) { 70 $ids_to_remove[] = intval( $mods['background_image_thumb'] ); 71 } 72 if ( ! empty( $mods['header_image_data'] ) && is_array( $mods['header_image_data'] ) && isset( $mods['header_image_data']['attachment_id'] ) ) { 73 $ids_to_remove[] = intval( $mods['header_image_data']['attachment_id'] ); 74 } 75 } 76 } 77 78 if ( ! empty( $ids_to_remove ) ) { 79 // Retrieve snapshot 80 $snapshot = get_option( 'media_tracker_unused_ids_snapshot', array() ); 81 if ( ! empty( $snapshot ) ) { 82 $original_count = count( $snapshot ); 83 $snapshot = array_diff( $snapshot, $ids_to_remove ); 84 85 if ( count( $snapshot ) !== $original_count ) { 86 // Update snapshot if changed 87 update_option( 'media_tracker_unused_ids_snapshot', $snapshot, false ); 88 } 89 } 90 91 // Also clear cache to be safe 92 $list = new Unused_Media_List( '', null ); 93 if ( method_exists( $list, 'force_clear_cache' ) ) { 94 $list->force_clear_cache(); 95 } 96 } 45 97 } 46 98 … … 48 100 * Add media page to WordPress admin menu. 49 101 */ 50 public function register_ unused_media_cleaner_menu() {102 public function register_media_tracker_menu() { 51 103 add_media_page( 52 __( ' Unused Media', 'media-tracker' ),53 __( ' Unused Media', 'media-tracker' ),104 __( 'Media Tracker', 'media-tracker' ), 105 __( 'Media Tracker', 'media-tracker' ), 54 106 'manage_options', 55 ' unused-media-cleaner',56 array( $this, ' uic_admin_page' )107 'media-tracker', 108 array( $this, 'media_tracker_admin_page' ) 57 109 ); 58 110 } 59 111 60 /** 61 * Callback function to render the unused media admin page content. 62 */ 63 public function uic_admin_page() { 64 // Suppress other plugin admin notices on this specific screen 65 if ( function_exists( 'remove_all_actions' ) ) { 66 remove_all_actions( 'admin_notices' ); 67 remove_all_actions( 'all_admin_notices' ); 68 remove_all_actions( 'user_admin_notices' ); 69 } 70 71 // phpcs:ignore WordPress.Security.NonceVerification.Recommended 72 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; 73 // phpcs:ignore WordPress.Security.NonceVerification.Recommended 74 $author_id = isset( $_GET['author'] ) ? intval( $_GET['author'] ) : null; 75 76 // Validate author_id if provided 77 if ( null !== $author_id && $author_id > 0 ) { 78 $author_user = get_userdata( $author_id ); 79 if ( false === $author_user ) { 80 $author_id = null; 81 } 82 } else { 83 $author_id = null; 84 } 85 86 // Create instance of Unused_Media_List and prepare items 87 $unused_media_list = new Unused_Media_List( $search, $author_id ); 88 $unused_media_list->prepare_items(); 89 90 // Include the view template for displaying the list 91 include __DIR__ . '/views/unused-media-list.php'; 92 } 93 94 /** 95 * Set the screen option value when it's updated. 96 */ 97 public function set_screen_option( $status, $option, $value ) { 98 if ( 'unused_media_cleaner_per_page' === $option ) { 99 return (int) $value; 100 } 101 return $status; 102 } 103 104 /** 105 * Add screen options for media page. 106 */ 107 public function add_screen_options() { 108 add_screen_option( 'per_page', array( 'label' => __( 'Unused Media per page', 'media-tracker' ), 'default' => 10, 'option' => 'unused_media_cleaner_per_page' ) ); 109 } 110 111 /** 112 * Suppress admin notices on the Unused Media screen to avoid other plugins' ads. 113 */ 114 public function suppress_admin_notices() { 115 if ( function_exists( 'remove_all_actions' ) ) { 116 remove_all_actions( 'admin_notices' ); 117 remove_all_actions( 'all_admin_notices' ); 118 remove_all_actions( 'user_admin_notices' ); 119 } 112 public function media_tracker_admin_page() { 113 include __DIR__ . '/views/media-tracker.php'; 120 114 } 121 115 … … 141 135 public function handle_get_scan_progress() { 142 136 if ( ! current_user_can( 'manage_options' ) ) { 143 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 ); 144 } 145 146 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 137 wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 ); 138 } 139 140 // Check nonce manually 141 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) { 142 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 ); 143 } 147 144 148 145 $progress_key = 'media_scan_progress_' . get_current_user_id(); … … 177 174 */ 178 175 public function handle_run_media_scan() { 179 if ( ! current_user_can( 'manage_options' ) ) { 180 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 ); 181 } 182 183 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 176 // Check user capabilities 177 if ( ! current_user_can( 'manage_options' ) ) { 178 wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 ); 179 } 180 181 // Check nonce manually to return proper JSON error 182 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) { 183 wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'media-tracker' ) ), 403 ); 184 } 184 185 185 186 $user_id = get_current_user_id(); … … 188 189 $progress_key = 'media_scan_progress_' . $user_id; 189 190 set_transient( $progress_key, array( 190 'step' => 1,191 'step' => 0, 191 192 'total_steps' => 6, 192 193 'current_step' => 'Starting scan...', … … 214 215 public function handle_run_media_scan_sync() { 215 216 if ( ! current_user_can( 'manage_options' ) ) { 216 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 ); 217 } 218 219 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 217 wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 ); 218 } 219 220 // Check nonce manually 221 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) { 222 wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'media-tracker' ) ), 403 ); 223 } 220 224 221 225 $user_id = get_current_user_id(); … … 224 228 // Initialize progress to visible state 225 229 $progress = array( 226 'step' => 1,230 'step' => 0, 227 231 'total_steps' => 6, 228 232 'current_step' => 'Starting scan...', … … 240 244 $list->force_clear_cache(); 241 245 } 242 $list->prepare_items(); 246 247 // Use new method to scan and save snapshot 248 if ( method_exists( $list, 'scan_and_save_snapshot' ) ) { 249 $list->scan_and_save_snapshot(); 250 } else { 251 // Fallback for safety 252 $list->prepare_items(); 253 } 243 254 244 255 // Mark complete … … 282 293 $progress = get_transient( $progress_key ); 283 294 if ( ! is_array( $progress ) ) { 284 $progress = array( 'step' => 1, 'total_steps' => 6, 'current_step' => 'Starting scan...', 'used_ids' => array() );295 $progress = array( 'step' => 0, 'total_steps' => 6, 'current_step' => 'Starting scan...', 'used_ids' => array() ); 285 296 } 286 297 $progress['current_step'] = 'Scanning...'; … … 290 301 $list->force_clear_cache(); 291 302 } 292 $list->prepare_items(); 303 304 // Use new method to scan and save snapshot 305 if ( method_exists( $list, 'scan_and_save_snapshot' ) ) { 306 $list->scan_and_save_snapshot(); 307 } else { 308 // Fallback for safety 309 $list->prepare_items(); 310 } 293 311 294 312 // Mark progress as complete … … 305 323 public function handle_clear_scan_progress() { 306 324 if ( ! current_user_can( 'manage_options' ) ) { 307 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 ); 308 } 309 310 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 325 wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 ); 326 } 327 328 // Check nonce manually 329 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) { 330 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 ); 331 } 311 332 312 333 $progress_key = 'media_scan_progress_' . get_current_user_id(); 313 334 delete_transient( $progress_key ); 314 335 336 // Also clear the dashboard stats cache so the overview tab reflects the new scan results 337 delete_transient( 'media_tracker_dashboard_stats_v6' ); 338 315 339 wp_send_json_success( array( 'message' => __( 'Progress cleared.', 'media-tracker' ) ) ); 316 340 } … … 321 345 public function handle_get_unused_media_count() { 322 346 if ( ! current_user_can( 'manage_options' ) ) { 323 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 ); 324 } 325 326 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 347 wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 ); 348 } 349 350 // Check nonce manually 351 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) { 352 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 ); 353 } 327 354 328 355 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; … … 344 371 ) ); 345 372 } 373 374 /** 375 * Handle AJAX request to remove all unused media. 376 */ 377 public function handle_remove_all_unused_media() { 378 if ( ! current_user_can( 'manage_options' ) ) { 379 wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 ); 380 } 381 382 // Check nonce manually 383 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) { 384 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 ); 385 } 386 387 // Get all unused media IDs using the new public method 388 $list = new Unused_Media_List( '', null ); 389 $unused_ids = $list->get_unused_media_ids( '' ); 390 391 $deleted_count = 0; 392 $failed_count = 0; 393 394 // Delete each unused media item 395 foreach ( $unused_ids as $post_id ) { 396 $result = wp_delete_attachment( $post_id, true ); // true for force delete (bypass trash) 397 if ( $result ) { 398 $deleted_count++; 399 } else { 400 $failed_count++; 401 } 402 } 403 404 // Clear the cache after deletion 405 if ( method_exists( $list, 'force_clear_cache' ) ) { 406 $list->force_clear_cache(); 407 } 408 409 if ( $deleted_count > 0 ) { 410 wp_send_json_success( array( 411 'message' => sprintf( 412 /* translators: %d: number of items deleted */ 413 _n( 'Successfully deleted %d unused media item.', 'Successfully deleted %d unused media items.', $deleted_count, 'media-tracker' ), 414 $deleted_count 415 ), 416 'deleted_count' => $deleted_count, 417 'failed_count' => $failed_count 418 ) ); 419 } else { 420 wp_send_json_error( array( 421 'message' => __( 'No unused media items found or failed to delete.', 'media-tracker' ) 422 ) ); 423 } 424 } 346 425 } -
media-tracker/trunk/includes/Admin/Unused_Media_List.php
r3432010 r3454648 6 6 7 7 use WP_List_Table; 8 9 // Define constants for safe scanning10 if ( ! defined( 'MEDIA_TRACKER_MAX_ITERATIONS' ) ) {11 define( 'MEDIA_TRACKER_MAX_ITERATIONS', 10000 ); // Maximum while loop iterations12 }13 14 if ( ! defined( 'MEDIA_TRACKER_BATCH_SIZE' ) ) {15 define( 'MEDIA_TRACKER_BATCH_SIZE', 100 ); // Batch size for processing16 }17 18 if ( ! defined( 'MEDIA_TRACKER_MAX_ATTACHMENT_ID' ) ) {19 define( 'MEDIA_TRACKER_MAX_ATTACHMENT_ID', 999999999 ); // Maximum valid attachment ID20 }21 8 22 9 /** … … 119 106 $author_url = add_query_arg( 120 107 [ 121 'page' => ' unused-media-cleaner',108 'page' => 'media-tracker', 122 109 'author' => $item->post_author, 123 110 ], … … 135 122 return date_i18n( 'Y/m/d', strtotime( $item->post_date ) ); 136 123 default: 137 return '';124 return isset( $item->$column_name ) ? esc_html( $item->$column_name ) : ''; 138 125 } 139 126 } … … 179 166 } 180 167 181 // Safely increase memory and time limits 182 if ( function_exists( 'ini_set' ) ) { 183 // Increase memory limit with fallback to current limit 184 $current_memory = ini_get( 'memory_limit' ); 185 if ( $current_memory && $current_memory !== '-1' ) { 186 // Convert to bytes for comparison 187 $current_bytes = wp_convert_hr_to_bytes( $current_memory ); 188 $desired_bytes = wp_convert_hr_to_bytes( '512M' ); 189 190 if ( $desired_bytes > $current_bytes ) { 191 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged 192 @ini_set( 'memory_limit', '512M' ); 193 } 194 } else { 195 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged 196 @ini_set( 'memory_limit', '512M' ); 197 } 198 199 // Increase execution time safely 200 if ( ! stristr( php_sapi_name(), 'cli' ) ) { 201 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged 202 @set_time_limit( 300 ); 203 } 168 wp_raise_memory_limit( 'admin' ); 169 if ( function_exists( 'set_time_limit' ) ) { 170 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Scanning can take a long time. 171 set_time_limit( 300 ); 204 172 } 205 173 … … 209 177 set_transient( $progress_key, $progress, 300 ); 210 178 211 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 212 $featured_images = $wpdb->get_col(" 179 $featured_images = $this->get_cached_db_result(" 213 180 SELECT DISTINCT meta_value 214 181 FROM {$wpdb->postmeta} … … 218 185 "); 219 186 if ( $featured_images ) { 220 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {221 $progress['used_ids'] = array();222 }223 187 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $featured_images ) ); 224 188 } … … 231 195 set_transient( $progress_key, $progress, 300 ); 232 196 233 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 234 $gallery_images = $wpdb->get_col(" 197 $gallery_images = $this->get_cached_db_result(" 235 198 SELECT DISTINCT meta_value 236 199 FROM {$wpdb->postmeta} … … 242 205 foreach ( $gallery_images as $gallery_string ) { 243 206 if ( ! empty( $gallery_string ) ) { 244 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {245 $progress['used_ids'] = array();246 }247 207 $gallery_ids = explode( ',', $gallery_string ); 248 208 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', array_filter( $gallery_ids ) ) ); … … 250 210 } 251 211 252 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 253 $variation_images = $wpdb->get_col(" 212 $variation_images = $this->get_cached_db_result(" 254 213 SELECT DISTINCT meta_value 255 214 FROM {$wpdb->postmeta} … … 263 222 "); 264 223 if ( $variation_images ) { 265 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {266 $progress['used_ids'] = array();267 }268 224 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $variation_images ) ); 269 225 } … … 271 227 } 272 228 273 // Step 2.5: WooCommerce Taxonomy Images (Categories, Tags, etc.) 229 $used_image_ids = $progress['used_ids']; 230 231 // Ensure featured images are always included 232 $featured_images = $this->get_cached_db_result(" 233 SELECT DISTINCT meta_value 234 FROM {$wpdb->postmeta} 235 WHERE meta_key = '_thumbnail_id' 236 AND meta_value != '0' 237 AND meta_value != '' 238 "); 239 if ( $featured_images ) { 240 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $featured_images ) ); 241 } 242 274 243 if ( class_exists( 'WooCommerce' ) ) { 275 // Get WooCommerce product category images 276 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 277 $category_images = $wpdb->get_col(" 278 SELECT tm.meta_value 279 FROM {$wpdb->termmeta} tm 280 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id 281 WHERE tt.taxonomy IN ('product_cat', 'product_tag') 282 AND tm.meta_key = 'thumbnail_id' 283 AND tm.meta_value != '' 284 AND tm.meta_value != '0' 244 $gallery_images = $this->get_cached_db_result(" 245 SELECT DISTINCT meta_value 246 FROM {$wpdb->postmeta} 247 WHERE meta_key = '_product_image_gallery' 248 AND meta_value != '' 249 AND meta_value IS NOT NULL 285 250 "); 286 251 287 if ( $category_images ) { 288 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) { 289 $progress['used_ids'] = array(); 290 } 291 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $category_images ) ); 292 } 293 } 294 295 // Ensure used_ids is set and is an array 296 $used_image_ids = isset( $progress['used_ids'] ) && is_array( $progress['used_ids'] ) 297 ? $progress['used_ids'] 298 : array(); 299 300 // Check for ACF using both class and function for better compatibility 301 if ( class_exists( 'ACF' ) || function_exists( 'acf' ) ) { 302 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 303 $acf_meta_values = $wpdb->get_col(" 252 foreach ( $gallery_images as $gallery_string ) { 253 if ( ! empty( $gallery_string ) ) { 254 $gallery_ids = explode( ',', $gallery_string ); 255 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', array_filter( $gallery_ids ) ) ); 256 } 257 } 258 259 $variation_images = $this->get_cached_db_result(" 260 SELECT DISTINCT meta_value 261 FROM {$wpdb->postmeta} 262 WHERE meta_key = '_thumbnail_id' 263 AND post_id IN ( 264 SELECT ID FROM {$wpdb->posts} 265 WHERE post_type = 'product_variation' 266 ) 267 AND meta_value != '0' 268 AND meta_value != '' 269 "); 270 if ( $variation_images ) { 271 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $variation_images ) ); 272 } 273 } 274 275 if ( class_exists( 'ACF' ) ) { 276 $acf_meta_values = $this->get_cached_db_result(" 304 277 SELECT DISTINCT pm.meta_value 305 278 FROM {$wpdb->postmeta} pm … … 326 299 327 300 if ( is_serialized( $meta_value ) ) { 328 $unserialized = unserialize( $meta_value );301 $unserialized = @unserialize( $meta_value ); 329 302 if ( is_array( $unserialized ) ) { 330 303 array_walk_recursive( $unserialized, function( $item ) use ( &$used_image_ids ) { 331 if ( is_numeric( $item ) && $item > 0 && $item < MEDIA_TRACKER_MAX_ATTACHMENT_ID) {304 if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) { 332 305 $used_image_ids[] = intval( $item ); 333 306 } … … 341 314 if ( is_array( $json_decoded ) ) { 342 315 array_walk_recursive( $json_decoded, function( $item ) use ( &$used_image_ids ) { 343 if ( is_numeric( $item ) && $item > 0 && $item < MEDIA_TRACKER_MAX_ATTACHMENT_ID) {316 if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) { 344 317 $used_image_ids[] = intval( $item ); 345 318 } … … 376 349 } 377 350 378 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 379 $acf_specific_query = $wpdb->get_col(" 351 $acf_specific_query = $this->get_cached_db_result(" 380 352 SELECT DISTINCT pm.meta_value 381 353 FROM {$wpdb->postmeta} pm … … 402 374 } else { 403 375 if ( is_serialized( $acf_value ) ) { 404 $unserialized = unserialize( $acf_value );376 $unserialized = @unserialize( $acf_value ); 405 377 if ( is_array( $unserialized ) ) { 406 378 array_walk_recursive( $unserialized, function( $item ) use ( &$used_image_ids ) { 407 if ( is_numeric( $item ) && $item > 0 && $item < MEDIA_TRACKER_MAX_ATTACHMENT_ID) {379 if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) { 408 380 $used_image_ids[] = intval( $item ); 409 381 } … … 415 387 } 416 388 417 $batch_size = MEDIA_TRACKER_BATCH_SIZE;389 $batch_size = 100; 418 390 $offset = 0; 419 $iterations = 0; 420 421 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 391 392 while ( true ) { 422 393 $sql = $wpdb->prepare( 423 394 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", … … 426 397 $offset 427 398 ); 428 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 429 $posts_with_content = $wpdb->get_results( $sql ); 399 $posts_with_content = $this->get_cached_db_result( $sql, 'results' ); 430 400 431 401 if ( empty( $posts_with_content ) ) { … … 436 406 preg_match_all( '/wp-image-(\d+)/', $post->post_content, $matches ); 437 407 if ( ! empty( $matches[1] ) ) { 438 if ( ! is_array( $used_image_ids ) ) {439 $used_image_ids = array();440 }441 408 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 442 409 } … … 444 411 445 412 $offset += $batch_size; 446 $iterations++;447 448 // Safety check: if we got fewer results than batch_size, we're done449 if ( count( $posts_with_content ) < $batch_size ) {450 break;451 }452 413 } 453 414 454 415 // Scan Gutenberg image blocks that may not include the wp-image- class 455 416 $offset_blocks = 0; 456 $iterations = 0; 457 458 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 417 while ( true ) { 459 418 $sql_blocks = $wpdb->prepare( 460 419 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", … … 463 422 $offset_blocks 464 423 ); 465 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 466 $posts_with_blocks = $wpdb->get_results( $sql_blocks ); 424 $posts_with_blocks = $this->get_cached_db_result( $sql_blocks, 'results' ); 467 425 468 426 if ( empty( $posts_with_blocks ) ) { … … 477 435 478 436 $offset_blocks += $batch_size; 479 $iterations++; 480 481 if ( count( $posts_with_blocks ) < $batch_size ) { 482 break; 483 } 484 } 485 486 // Scan Gutenberg cover blocks with background images 487 $offset_cover = 0; 488 $iterations = 0; 489 490 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 491 $sql_cover = $wpdb->prepare( 492 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", 437 } 438 439 // Scan other Gutenberg media blocks (Cover, Media & Text, Video, Audio, File) 440 $offset_blocks_ext = 0; 441 while ( true ) { 442 $sql_blocks_ext = $wpdb->prepare( 443 "SELECT ID, post_content FROM {$wpdb->posts} WHERE ( 444 post_content LIKE %s OR 445 post_content LIKE %s OR 446 post_content LIKE %s OR 447 post_content LIKE %s OR 448 post_content LIKE %s 449 ) AND post_status = 'publish' LIMIT %d OFFSET %d", 493 450 '%wp:cover%', 494 $batch_size,495 $offset_cover496 );497 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching498 $posts_with_cover = $wpdb->get_results( $sql_cover );499 500 if ( empty( $posts_with_cover ) ) {501 break;502 }503 504 foreach ( $posts_with_cover as $post ) {505 // Match: <!-- wp:cover {"url":"...","id":123} -->506 if ( preg_match_all( '/<!--\s*wp:cover\s*{[^}]*"id"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {507 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );508 }509 }510 511 $offset_cover += $batch_size;512 $iterations++;513 514 if ( count( $posts_with_cover ) < $batch_size ) {515 break;516 }517 }518 519 // Scan Gutenberg media-text blocks520 $offset_mediatext = 0;521 $iterations = 0;522 523 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {524 $sql_mediatext = $wpdb->prepare(525 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",526 451 '%wp:media-text%', 527 $batch_size, 528 $offset_mediatext 529 ); 530 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 531 $posts_with_mediatext = $wpdb->get_results( $sql_mediatext ); 532 533 if ( empty( $posts_with_mediatext ) ) { 534 break; 535 } 536 537 foreach ( $posts_with_mediatext as $post ) { 538 // Match: <!-- wp:media-text {"mediaId":123} --> 539 if ( preg_match_all( '/<!--\s*wp:media-text\s*{[^}]*"mediaId"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 540 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 541 } 542 } 543 544 $offset_mediatext += $batch_size; 545 $iterations++; 546 547 if ( count( $posts_with_mediatext ) < $batch_size ) { 548 break; 549 } 550 } 551 552 // Scan Gutenberg file blocks 553 $offset_file = 0; 554 $iterations = 0; 555 556 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 557 $sql_file = $wpdb->prepare( 558 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", 452 '%wp:video%', 453 '%wp:audio%', 559 454 '%wp:file%', 560 455 $batch_size, 561 $offset_file 562 ); 563 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 564 $posts_with_file = $wpdb->get_results( $sql_file ); 565 566 if ( empty( $posts_with_file ) ) { 456 $offset_blocks_ext 457 ); 458 $posts_with_blocks_ext = $this->get_cached_db_result( $sql_blocks_ext, 'results' ); 459 460 if ( empty( $posts_with_blocks_ext ) ) { 567 461 break; 568 462 } 569 463 570 foreach ( $posts_with_ fileas $post ) {571 // Match: <!-- wp:file {"id":123} -->572 if ( preg_match_all( '/<!--\s*wp: file\s*{[^}]*"id"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {464 foreach ( $posts_with_blocks_ext as $post ) { 465 // Cover block 466 if ( preg_match_all( '/<!--\s*wp:cover\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 573 467 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 574 468 } 575 } 576 577 $offset_file += $batch_size; 578 $iterations++; 579 580 if ( count( $posts_with_file ) < $batch_size ) { 581 break; 582 } 583 } 584 585 // Scan Gutenberg video and audio blocks 586 $offset_media = 0; 587 $iterations = 0; 588 589 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 590 $sql_media = $wpdb->prepare( 591 "SELECT ID, post_content FROM {$wpdb->posts} WHERE (post_content LIKE %s OR post_content LIKE %s) AND post_status = 'publish' LIMIT %d OFFSET %d", 592 '%wp:video%', 593 '%wp:audio%', 594 $batch_size, 595 $offset_media 596 ); 597 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 598 $posts_with_media = $wpdb->get_results( $sql_media ); 599 600 if ( empty( $posts_with_media ) ) { 601 break; 602 } 603 604 foreach ( $posts_with_media as $post ) { 605 // Match video/audio poster images: <!-- wp:video {"poster":123,"id":456} --> 606 if ( preg_match_all( '/<!--\s*wp:(video|audio)\s*{[^}]*"id"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 607 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[2] ) ); 608 } 609 // Match poster images separately 610 if ( preg_match_all( '/<!--\s*wp:(video|audio)\s*{[^}]*"poster"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 611 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[2] ) ); 612 } 613 } 614 615 $offset_media += $batch_size; 616 $iterations++; 617 618 if ( count( $posts_with_media ) < $batch_size ) { 619 break; 620 } 621 } 622 623 // Scan old caption shortcodes with attachment IDs 624 $offset_caption = 0; 625 $iterations = 0; 626 627 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 628 $sql_caption = $wpdb->prepare( 629 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", 630 '%[caption%', 631 $batch_size, 632 $offset_caption 633 ); 634 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 635 $posts_with_caption = $wpdb->get_results( $sql_caption ); 636 637 if ( empty( $posts_with_caption ) ) { 638 break; 639 } 640 641 foreach ( $posts_with_caption as $post ) { 642 // Match: [caption id="attachment_123" ...] or [caption id="123" ...] 643 if ( preg_match_all( '/\[caption[^\]]*id\s*=\s*["\']?attachment_(\d+)["\']?/', $post->post_content, $matches ) ) { 469 // Media & Text block (uses mediaId) 470 if ( preg_match_all( '/<!--\s*wp:media-text\s*{[^}]*\"mediaId\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 644 471 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 645 472 } 646 // Also match caption with numeric id without attachment_ prefix647 if ( preg_match_all( '/ \[caption[^\]]*id\s*=\s*["\']?(\d+)["\']?(?:[^\]]*\s*class\s*=[^\]]*wp-caption)/', $post->post_content, $matches ) ) {473 // Video block 474 if ( preg_match_all( '/<!--\s*wp:video\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 648 475 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 649 476 } 650 } 651 652 $offset_caption += $batch_size; 653 $iterations++; 654 655 if ( count( $posts_with_caption ) < $batch_size ) { 656 break; 657 } 477 // Audio block 478 if ( preg_match_all( '/<!--\s*wp:audio\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 479 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 480 } 481 // File block 482 if ( preg_match_all( '/<!--\s*wp:file\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 483 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 484 } 485 } 486 487 $offset_blocks_ext += $batch_size; 658 488 } 659 489 660 490 // Scan gallery shortcodes and Gutenberg gallery blocks 661 491 $offset_gallery = 0; 662 $iterations = 0; 663 664 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 492 while ( true ) { 665 493 $sql_gallery = $wpdb->prepare( 666 494 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", … … 669 497 $offset_gallery 670 498 ); 671 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 672 $posts_with_gallery = $wpdb->get_results( $sql_gallery ); 499 $posts_with_gallery = $this->get_cached_db_result( $sql_gallery, 'results' ); 673 500 674 501 if ( empty( $posts_with_gallery ) ) { … … 696 523 697 524 $offset_gallery += $batch_size; 698 $iterations++;699 700 if ( count( $posts_with_gallery ) < $batch_size ) {701 break;702 }703 525 } 704 526 705 527 // Elementor: scan all posts with _elementor_data in batches and extract image IDs more accurately 706 528 $el_offset = 0; 707 $iterations = 0; 708 709 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 529 while ( true ) { 710 530 $sql_el = $wpdb->prepare( 711 531 "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_elementor_data' LIMIT %d OFFSET %d", … … 713 533 $el_offset 714 534 ); 715 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 716 $elementor_posts = $wpdb->get_col( $sql_el ); 535 $elementor_posts = $this->get_cached_db_result( $sql_el ); 717 536 718 537 if ( empty( $elementor_posts ) ) { … … 740 559 } 741 560 } 561 562 // Check for 'ids' key (arrays or comma-separated strings), common in galleries 563 if ( isset( $node['ids'] ) ) { 564 $ids_val = $node['ids']; 565 if ( is_array( $ids_val ) ) { 566 foreach ( $ids_val as $id ) { 567 if ( is_numeric( $id ) && $id > 0 ) { 568 $used_image_ids[] = intval( $id ); 569 } 570 } 571 } elseif ( is_string( $ids_val ) ) { 572 $ids = array_filter( array_map( 'intval', explode( ',', $ids_val ) ) ); 573 if ( ! empty( $ids ) ) { 574 $used_image_ids = array_merge( $used_image_ids, $ids ); 575 } 576 } 577 } 578 742 579 foreach ( $node as $v ) { 743 580 $extract_ids( $v ); … … 750 587 751 588 $el_offset += $batch_size; 752 $iterations++;753 754 if ( count( $elementor_posts ) < $batch_size ) {755 break;756 }757 589 } 758 590 759 591 // Divi Builder: scan posts using Divi shortcodes and extract image IDs/URLs 760 592 $divi_offset = 0; 761 $iterations = 0; 762 763 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 593 while ( true ) { 764 594 $sql_divi = $wpdb->prepare( 765 595 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d", … … 768 598 $divi_offset 769 599 ); 770 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 771 $divi_posts = $wpdb->get_results( $sql_divi ); 600 $divi_posts = $this->get_cached_db_result( $sql_divi, 'results' ); 772 601 773 602 if ( empty( $divi_posts ) ) { … … 810 639 811 640 $divi_offset += $batch_size; 812 $iterations++;813 814 if ( count( $divi_posts ) < $batch_size ) {815 break;816 }817 641 } 818 642 819 643 // Generic: scan direct uploads URLs in post content and map to attachment IDs 820 644 $offset_urls = 0; 821 $iterations = 0; 822 823 while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) { 645 while ( true ) { 824 646 $sql_urls = $wpdb->prepare( 825 647 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d", … … 828 650 $offset_urls 829 651 ); 830 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 831 $posts_with_urls = $wpdb->get_results( $sql_urls ); 652 $posts_with_urls = $this->get_cached_db_result( $sql_urls, 'results' ); 832 653 833 654 if ( empty( $posts_with_urls ) ) { … … 848 669 849 670 $offset_urls += $batch_size; 850 $iterations++; 851 852 if ( count( $posts_with_urls ) < $batch_size ) { 671 } 672 673 // Scan post_excerpt (e.g. WooCommerce Short Description) 674 $offset_excerpt = 0; 675 while ( true ) { 676 $sql_excerpt = $wpdb->prepare( 677 "SELECT ID, post_excerpt FROM {$wpdb->posts} WHERE post_status = 'publish' AND (post_excerpt LIKE %s OR post_excerpt LIKE %s) LIMIT %d OFFSET %d", 678 '%wp-content/uploads%', 679 '%wp-image-%', 680 $batch_size, 681 $offset_excerpt 682 ); 683 $posts_with_excerpt = $this->get_cached_db_result( $sql_excerpt, 'results' ); 684 685 if ( empty( $posts_with_excerpt ) ) { 853 686 break; 854 687 } 688 689 foreach ( $posts_with_excerpt as $post ) { 690 // Check for wp-image- ID 691 if ( preg_match_all( '/wp-image-(\d+)/', $post->post_excerpt, $matches ) ) { 692 if ( ! empty( $matches[1] ) ) { 693 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 694 } 695 } 696 697 // Check for direct URLs 698 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $post->post_excerpt, $m_url_all ) ) { 699 foreach ( $m_url_all[0] as $url ) { 700 $id = attachment_url_to_postid( $url ); 701 if ( $id ) { 702 $used_image_ids[] = intval( $id ); 703 } 704 } 705 } 706 } 707 708 $offset_excerpt += $batch_size; 709 } 710 711 // Scan postmeta for raw URLs (custom fields, metaboxes) 712 $offset_meta = 0; 713 while ( true ) { 714 $sql_meta = $wpdb->prepare( 715 "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_value LIKE %s LIMIT %d OFFSET %d", 716 '%wp-content/uploads%', 717 $batch_size, 718 $offset_meta 719 ); 720 $meta_values = $this->get_cached_db_result( $sql_meta, 'col' ); 721 722 if ( empty( $meta_values ) ) { 723 break; 724 } 725 726 foreach ( $meta_values as $val ) { 727 // Check for direct URLs 728 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all ) ) { 729 foreach ( $m_url_all[0] as $url ) { 730 // Clean up URL (remove query strings, etc if needed, though attachment_url_to_postid handles some) 731 $id = attachment_url_to_postid( $url ); 732 if ( $id ) { 733 $used_image_ids[] = intval( $id ); 734 } 735 } 736 } 737 } 738 $offset_meta += $batch_size; 739 } 740 741 // Scan options for raw URLs (theme settings, custom options) 742 $offset_options = 0; 743 while ( true ) { 744 $sql_options = $wpdb->prepare( 745 "SELECT option_value FROM {$wpdb->options} WHERE option_value LIKE %s LIMIT %d OFFSET %d", 746 '%wp-content/uploads%', 747 $batch_size, 748 $offset_options 749 ); 750 $option_values = $this->get_cached_db_result( $sql_options, 'col' ); 751 752 if ( empty( $option_values ) ) { 753 break; 754 } 755 756 foreach ( $option_values as $val ) { 757 // Check for direct URLs 758 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all ) ) { 759 foreach ( $m_url_all[0] as $url ) { 760 $id = attachment_url_to_postid( $url ); 761 if ( $id ) { 762 $used_image_ids[] = intval( $id ); 763 } 764 } 765 } 766 } 767 $offset_options += $batch_size; 855 768 } 856 769 … … 876 789 877 790 $used_image_ids = array_unique( array_filter( array_map( 'intval', $used_image_ids ), function( $id ) { 878 return $id > 0 && $id < MEDIA_TRACKER_MAX_ATTACHMENT_ID;791 return $id > 0 && $id < 999999999; 879 792 })); 880 793 881 794 return $used_image_ids; 795 } 796 797 public function scan_and_save_snapshot() { 798 global $wpdb; 799 800 // Force calculation of used IDs 801 $used_image_ids = $this->get_used_media_ids(); 802 803 // Get all attachment IDs 804 $all_attachments = $this->get_cached_db_result( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit'" ); 805 806 // Calculate unused IDs 807 $unused_ids = array_diff( $all_attachments, $used_image_ids ); 808 809 // Filter and sanitize 810 $unused_ids = array_unique( array_filter( array_map( 'intval', $unused_ids ) ) ); 811 812 // Calculate size 813 $total_size = 0; 814 foreach ( $unused_ids as $id ) { 815 $file_path = get_attached_file( $id ); 816 if ( $file_path && file_exists( $file_path ) ) { 817 $total_size += filesize( $file_path ); 818 } 819 } 820 821 // Save snapshot and stats 822 update_option( 'media_tracker_unused_ids_snapshot', $unused_ids, false ); 823 update_option( 'unused_media_last_cache_time', time() ); 824 update_option( 'media_tracker_unused_count_last_scan', count( $unused_ids ) ); 825 update_option( 'media_tracker_unused_size_last_scan', $total_size ); 826 827 // Invalidate dashboard stats cache to ensure overview tab is updated 828 delete_transient( 'media_tracker_dashboard_stats_v8' ); 829 830 return count($unused_ids); 882 831 } 883 832 … … 887 836 $this->display_delete_message(); 888 837 889 // Verify nonce for search and filter actions 890 if ( isset( $_REQUEST['s'] ) || isset( $_GET['refresh_cache'] ) ) { 891 check_admin_referer( 'unused-media-filter' ); 892 } 893 894 $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; 838 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Search request is a GET request and safe. 839 $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; 895 840 896 841 $per_page = $this->get_items_per_page( 'unused_media_cleaner_per_page', 10 ); … … 898 843 $offset = ( $current_page - 1 ) * $per_page; 899 844 900 $force_refresh = isset( $_GET['refresh_cache'] ) && $_GET['refresh_cache'] === '1'; 901 902 $cache_key = 'unused_media_list_v2_' . md5( serialize( array( $search, $this->author_id, $current_page, $per_page ) ) ); 903 904 if ( $force_refresh || $this->should_invalidate_cache() ) { 905 delete_transient( $cache_key ); 906 } 907 908 $cached_results = get_transient( $cache_key ); 909 910 if ( false === $cached_results || $force_refresh ) { 911 $used_image_ids = $this->get_used_media_ids(); 912 913 $where_conditions = [ 914 "p.post_type = 'attachment'", 915 "p.post_status = 'inherit'" 916 ]; 917 918 if ( ! empty( $used_image_ids ) ) { 919 // Ensure all IDs are integers and sanitize 920 $sanitized_ids = array_map( 'intval', $used_image_ids ); 921 $sanitized_ids = array_filter( $sanitized_ids, function( $id ) { 922 return $id > 0; 923 } ); 924 925 if ( ! empty( $sanitized_ids ) ) { 926 // Escape IDs properly to avoid SQL injection 927 $escaped_ids = implode( ',', array_map( 'absint', $sanitized_ids ) ); 928 $where_conditions[] = "p.ID NOT IN ($escaped_ids)"; 929 } 930 } 845 // Retrieve from snapshot 846 $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() ); 847 848 // Handle search if needed (filter snapshot IDs by search term) 849 // This requires a query if search is present, but restricted to snapshot IDs. 850 851 $where_conditions = [ 852 "p.post_type = 'attachment'", 853 "p.post_status = 'inherit'" 854 ]; 855 856 if ( empty( $unused_image_ids ) ) { 857 // No unused media found in snapshot (or not scanned yet) 858 $this->items = array(); 859 $total_items = 0; 860 } else { 861 $args = array( 862 'post_type' => 'attachment', 863 'post_status' => 'inherit', 864 'post__in' => $unused_image_ids, 865 'posts_per_page' => $per_page, 866 'paged' => $current_page, 867 'orderby' => $orderby_param, 868 'order' => $order_param, 869 ); 931 870 932 871 if ( $this->author_id ) { 933 $ where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id );872 $args['author'] = $this->author_id; 934 873 } 935 874 936 875 if ( $search ) { 937 $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' ); 938 } 939 940 $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); 941 942 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 943 $count_query = " 944 SELECT COUNT(*) 945 FROM {$wpdb->posts} p 946 $where_clause 947 "; 948 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 949 $total_items = $wpdb->get_var( $count_query ); 950 951 // Construct the base query with WHERE clause - phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 952 // The $where_clause is safely constructed from sanitized/prepared values 953 $base_query = "SELECT p.ID, p.post_title, p.guid, p.post_author, p.post_date 954 FROM {$wpdb->posts} p 955 $where_clause 956 ORDER BY p.post_date DESC"; 957 958 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 959 $this->items = $wpdb->get_results( $wpdb->prepare( $base_query . ' LIMIT %d, %d', $offset, $per_page ) ); 960 961 set_transient( $cache_key, array( 'items' => $this->items, 'total_items' => $total_items ), 1800 ); 962 963 update_option( 'unused_media_last_cache_time', time() ); 964 } else { 965 $this->items = $cached_results['items']; 966 $total_items = $cached_results['total_items']; 876 $args['s'] = $search; 877 } 878 879 $query = new \WP_Query( $args ); 880 $this->items = $query->posts; 881 $total_items = $query->found_posts; 967 882 } 968 883 … … 975 890 'total_items' => $total_items, 976 891 'per_page' => $per_page, 977 'total_pages' => ceil( $total_items / $per_page ),978 ) );892 'total_pages' => $per_page > 0 ? ceil( $total_items / $per_page ) : 0, 893 ) ); 979 894 } 980 895 … … 991 906 $last_cache_time = get_option( 'unused_media_last_cache_time', 0 ); 992 907 993 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 994 $recent_media_activity = $wpdb->get_var( $wpdb->prepare( 995 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_modified_gmt > %s", 908 $recent_media_activity = $this->get_cached_db_result( $wpdb->prepare( 909 "SELECT COUNT(*)\n FROM {$wpdb->posts}\n WHERE post_type = 'attachment'\n AND post_modified_gmt > %s", 996 910 gmdate( 'Y-m-d H:i:s', $last_cache_time ) 997 ) ); 998 999 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 1000 $recent_post_activity = $wpdb->get_var( $wpdb->prepare( 1001 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type IN ('post', 'page', 'product') AND post_modified_gmt > %s", 911 ), 'var' ); 912 913 $recent_post_activity = $this->get_cached_db_result( $wpdb->prepare( 914 "SELECT COUNT(*)\n FROM {$wpdb->posts}\n WHERE post_type IN ('post', 'page', 'product')\n AND post_modified_gmt > %s", 1002 915 gmdate( 'Y-m-d H:i:s', $last_cache_time ) 1003 ) );916 ), 'var' ); 1004 917 1005 918 return ( $recent_media_activity > 0 || $recent_post_activity > 0 ); … … 1014 927 global $wpdb; 1015 928 1016 // Verify nonce for cache refresh actions 1017 if ( isset( $_GET['refresh_cache'] ) ) { 1018 check_admin_referer( 'unused-media-filter' ); 1019 } 1020 1021 $force_refresh = ( isset( $_GET['refresh_cache'] ) && $_GET['refresh_cache'] === '1' ) || $force_fresh; 1022 1023 $cache_key = 'unused_media_total_' . md5( serialize( array( $search, $this->author_id ) ) ); 1024 1025 if ( $force_refresh || $this->should_invalidate_cache() ) { 1026 delete_transient( $cache_key ); 1027 } 1028 1029 $cached_total = get_transient( $cache_key ); 1030 1031 if ( false !== $cached_total && ! $force_refresh ) { 1032 return $cached_total; 1033 } 1034 1035 $used_image_ids = $this->get_used_media_ids(); 929 // Retrieve from snapshot 930 $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() ); 931 932 if ( empty( $unused_image_ids ) ) { 933 return 0; 934 } 1036 935 1037 936 $where_conditions = [ … … 1040 939 ]; 1041 940 1042 if ( ! empty( $used_image_ids ) ) { 1043 // Ensure all IDs are integers and sanitize 1044 $sanitized_ids = array_map( 'intval', $used_image_ids ); 1045 $sanitized_ids = array_filter( $sanitized_ids, function( $id ) { 1046 return $id > 0; 1047 } ); 1048 1049 if ( ! empty( $sanitized_ids ) ) { 1050 // Escape IDs properly to avoid SQL injection 1051 $escaped_ids = implode( ',', array_map( 'absint', $sanitized_ids ) ); 1052 $where_conditions[] = "p.ID NOT IN ($escaped_ids)"; 1053 } 1054 } 941 $ids_placeholder = implode( ',', array_map( 'intval', $unused_image_ids ) ); 942 $where_conditions[] = "p.ID IN ($ids_placeholder)"; 1055 943 1056 944 if ( $this->author_id ) { … … 1064 952 $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); 1065 953 1066 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared1067 954 $query = " 1068 955 SELECT COUNT(*) … … 1070 957 $where_clause 1071 958 "; 1072 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 1073 $total_items = $wpdb->get_var( $query ); 1074 1075 set_transient( $cache_key, $total_items, 1800 ); 1076 1077 update_option( 'unused_media_last_cache_time', time() ); 1078 1079 return $total_items; 959 960 return $this->get_cached_db_result( $query, 'var' ); 1080 961 } 1081 962 1082 963 public function get_fresh_total_items( $search = '' ) { 1083 964 return $this->get_total_items( $search, true ); 965 } 966 967 /** 968 * Get all unused media IDs. 969 * 970 * @param string $search Optional search term. 971 * @return array Array of unused media IDs. 972 */ 973 public function get_unused_media_ids( $search = '' ) { 974 global $wpdb; 975 976 // Retrieve from snapshot 977 $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() ); 978 979 if ( empty( $unused_image_ids ) ) { 980 return array(); 981 } 982 983 // Build query to get all unused media 984 $where_conditions = [ 985 "p.post_type = 'attachment'", 986 "p.post_status = 'inherit'" 987 ]; 988 989 $ids_placeholder = implode( ',', array_map( 'intval', $unused_image_ids ) ); 990 $where_conditions[] = "p.ID IN ($ids_placeholder)"; 991 992 if ( $this->author_id ) { 993 $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id ); 994 } 995 996 if ( $search ) { 997 $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' ); 998 } 999 1000 $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); 1001 1002 $query = " 1003 SELECT p.ID 1004 FROM {$wpdb->posts} p 1005 $where_clause 1006 "; 1007 1008 return $this->get_cached_db_result( $query ); 1084 1009 } 1085 1010 … … 1125 1050 global $wpdb; 1126 1051 1127 // Use WordPress cache API for better compatibility 1128 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 1129 $transients = $wpdb->get_col( $wpdb->prepare( 1130 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", 1131 '_transient_unused_media_%' 1132 ) ); 1133 1134 foreach ( $transients as $transient ) { 1135 $key = str_replace( '_transient_', '', $transient ); 1136 delete_transient( $key ); 1137 } 1052 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 1053 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_unused_media_%'" ); 1054 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 1055 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_unused_media_%'" ); 1138 1056 1139 1057 delete_option( 'unused_media_last_cache_time' ); 1058 } 1059 1060 /** 1061 * Helper to execute DB queries with caching. 1062 * 1063 * @param string $query The SQL query. 1064 * @param string $type The type of query result ('col', 'var', 'results'). 1065 * @return mixed Query result. 1066 */ 1067 private function get_cached_db_result( $query, $type = 'col' ) { 1068 global $wpdb; 1069 1070 $key = 'mt_db_' . md5( $query ); 1071 $group = 'media_tracker'; 1072 $result = wp_cache_get( $key, $group ); 1073 1074 if ( false === $result ) { 1075 if ( 'col' === $type ) { 1076 $result = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared 1077 } elseif ( 'results' === $type ) { 1078 $result = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared 1079 } elseif ( 'var' === $type ) { 1080 $result = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared 1081 } 1082 wp_cache_set( $key, $result, $group, 300 ); 1083 } 1084 return $result; 1140 1085 } 1141 1086 -
media-tracker/trunk/includes/Admin/views/unused-media-list.php
r3432010 r3454648 11 11 wp_localize_script( 'mt-admin-script', 'mediaTracker', array( 12 12 'nonce' => wp_create_nonce( 'media_tracker_nonce' ), 13 'ajax _url' => admin_url( 'admin-ajax.php' ),13 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 14 14 ) ); 15 15 } 16 17 16 ?> 18 17 19 <div class="wrap unused-media-list"> 20 <h1 class="wp-heading-inline"> 21 <svg fill="#fff" width="40px" height="40px" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" stroke="#fff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M46.4375 0.03125C45.539063 0.0390625 44.695313 0.398438 44.21875 1.125L36.625 15.40625C37.1875 15.601563 38.453125 16.164063 42.65625 18.0625L42.71875 18.09375C43.445313 18.421875 44 18.65625 44.21875 18.75C44.292969 18.785156 44.363281 18.839844 44.4375 18.875L49.96875 3.5625C50.316406 2.351563 49.449219 0.957031 48.0625 0.40625C47.546875 0.148438 46.976563 0.0273438 46.4375 0.03125 Z M 4 8C1.792969 8 0 9.792969 0 12C0 14.207031 1.792969 16 4 16C6.207031 16 8 14.207031 8 12C8 9.792969 6.207031 8 4 8 Z M 13 11C11.894531 11 11 11.894531 11 13C11 14.105469 11.894531 15 13 15C14.105469 15 15 14.105469 15 13C15 11.894531 14.105469 11 13 11 Z M 32.15625 16.625C30.222656 16.769531 28.539063 17.730469 27.34375 19.40625C28.097656 20.675781 29.417969 22.226563 31.28125 22.1875C31.773438 22.167969 32.1875 22.523438 32.28125 23C32.660156 23.589844 34.988281 24.636719 35.65625 24.375C35.9375 24.265625 36.238281 24.289063 36.5 24.4375C36.761719 24.585938 36.949219 24.828125 37 25.125C37.039063 25.289063 37.476563 25.863281 38.375 26.28125C39.082031 26.609375 39.769531 26.691406 40.15625 26.5C40.40625 26.375 40.679688 26.371094 40.9375 26.46875C41.199219 26.566406 41.425781 26.773438 41.53125 27.03125C42.207031 28.679688 45.292969 28.800781 47.40625 28.625C47.714844 27.285156 47.632813 25.890625 47.15625 24.59375C46.496094 22.808594 45.1875 21.398438 43.40625 20.59375C43.21875 20.511719 42.613281 20.222656 41.84375 19.875C38.28125 18.265625 36.269531 17.390625 35.875 17.28125C34.570313 16.765625 33.316406 16.539063 32.15625 16.625 Z M 11.5 18C8.46875 18 6 20.46875 6 23.5C6 26.53125 8.46875 29 11.5 29C14.53125 29 17 26.53125 17 23.5C17 20.46875 14.53125 18 11.5 18 Z M 26.28125 21.40625C25.96875 22.148438 25.613281 22.84375 25.25 23.5C25.679688 24.546875 26.949219 26.972656 29.28125 26.4375C29.550781 26.375 29.835938 26.410156 30.0625 26.5625C30.292969 26.714844 30.421875 26.949219 30.46875 27.21875C30.535156 27.59375 30.976563 28.039063 31.59375 28.375C32.46875 28.847656 33.414063 28.953125 33.8125 28.78125C34.074219 28.667969 34.367188 28.660156 34.625 28.78125C34.882813 28.902344 35.078125 29.132813 35.15625 29.40625C35.296875 29.882813 35.789063 30.371094 36.46875 30.71875C37.269531 31.125 38.183594 31.273438 38.78125 31.0625C39.242188 30.902344 39.734375 31.097656 39.96875 31.53125C40.851563 33.167969 43.75 33.34375 46 33.1875C46.285156 32.375 46.550781 31.539063 46.8125 30.65625C46.542969 30.671875 46.261719 30.6875 45.96875 30.6875C43.875 30.6875 41.371094 30.273438 40.125 28.5625C39.28125 28.675781 38.3125 28.492188 37.34375 28C36.640625 27.640625 35.867188 27.089844 35.40625 26.40625C34.132813 26.40625 32.667969 25.699219 31.9375 25.25C31.371094 24.902344 30.929688 24.558594 30.65625 24.1875C28.671875 24.003906 27.253906 22.710938 26.28125 21.40625 Z M 24 25.46875C17.800781 34.082031 7.214844 33.828125 7.09375 33.8125C6.699219 33.777344 6.3125 33.988281 6.125 34.34375C5.9375 34.699219 5.964844 35.125 6.21875 35.4375C8.003906 37.640625 9.921875 39.503906 11.875 41.09375C12.796875 41.277344 18.597656 42.097656 24.34375 35.4375C24.703125 35.019531 25.332031 34.984375 25.75 35.34375C26.167969 35.703125 26.203125 36.332031 25.84375 36.75C21.835938 41.394531 17.609375 42.847656 14.65625 43.15625C17.125 44.820313 19.613281 46.078125 21.9375 47.03125C23.414063 46.722656 28.367188 45.242188 32.75 38.5625C33.054688 38.101563 33.695313 37.945313 34.15625 38.25C34.617188 38.554688 34.742188 39.195313 34.4375 39.65625C31.132813 44.691406 27.515625 47.054688 24.96875 48.15625C30.167969 49.839844 34.046875 49.988281 34.375 50L34.40625 50C34.59375 50 34.777344 49.945313 34.9375 49.84375C35.21875 49.667969 41.007813 45.886719 45.25 35.25C45.085938 35.253906 44.917969 35.28125 44.75 35.28125C42.5625 35.28125 40.035156 34.839844 38.65625 33.125C37.6875 33.242188 36.578125 33.019531 35.5625 32.5C34.734375 32.074219 34.078125 31.503906 33.65625 30.84375C32.59375 30.933594 31.445313 30.550781 30.65625 30.125C29.84375 29.683594 29.207031 29.128906 28.84375 28.5C26.542969 28.621094 24.945313 27.054688 24 25.46875Z"></path></g></svg> 22 <?php echo esc_html__( 'Unused Media Files', 'media-tracker' ); ?> 23 </h1> 24 25 <div class="media-toolbar-wrap wp-filter"> 18 <div class="unused-media-list"> 19 <div class="media-header"> 20 <div class="section-title"> 21 <h2><i class="dashicons dashicons-format-image"></i> <?php esc_html_e( 'Unused Media', 'media-tracker' ); ?></h2> 22 <p class="page-subtitle"> 23 <?php esc_html_e( 'Detect media files not used anywhere on your site for safe cleanup.', 'media-tracker' ); ?> 24 </p> 25 </div> 26 26 27 <div class="unused-image-found"> 27 28 <?php 28 29 // Use the same cached total as the table to ensure consistency. 29 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound 30 // phpcs:ignore WordPress.Security.NonceVerification.Recommended 31 $mt_search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.Security.NonceVerification.Recommended 32 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound 33 $mt_initial_count = $unused_media_list->get_total_items( $mt_search ); 30 $media_tracker_search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 31 $media_tracker_initial_count = $media_tracker_unused_media_list->get_total_items( $media_tracker_search ); 34 32 ?> 35 <h2><?php echo '<span id="unused-count">'.esc_html( $mt_initial_count ).'</span>' . ' ' . esc_html__( 'Unused Media Found!', 'media-tracker' ); ?></h2> 36 </div> 37 38 <div class="search-form"> 39 <div class="media-scan-controls" style="margin: 16px 0;"> 40 <button id="run-media-scan" class="button button-primary"> 41 <?php 42 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound 43 $last_scan_time = (int) get_option( 'unused_media_last_cache_time', 0 ); 44 echo esc_html( $last_scan_time ? __( 'Rescan', 'media-tracker' ) : __( 'Run Scan in Background', 'media-tracker' ) ); 45 ?> 46 </button> 47 </div> 48 49 <?php $unused_media_list->search_box( esc_html__( 'Search Media', 'media-tracker' ), 'media-search' ); ?> 33 <h2><?php echo '<span id="unused-count">'.esc_html( $media_tracker_initial_count ).'</span>' . ' ' . esc_html__( 'unused media found!', 'media-tracker' ); ?></h2> 50 34 </div> 51 35 </div> 52 36 53 <div id="media-scan-progress" style="display: none; margin: 16px 0 ; padding: 12px; border: 1px solid #ccd0d4; background: #fff;">37 <div id="media-scan-progress" style="display: none; margin: 16px 0 20px; padding: 14px 15px 18px; border: 1px solid #e0e0e0; border-radius: 3px;"> 54 38 <p id="media-scan-progress-text" style="margin: 0 0 8px;"> 55 39 <?php esc_html_e( 'Scan status: Ready to scan...', 'media-tracker' ); ?> 56 40 </p> 57 <div id="media-scan-progress-bar" style="position: relative; height: 16px; background: #f0f0f0; border: 1px solid #ccd0d4;"> 58 <div id="media-scan-progress-fill" style="height: 100%; width: 0; background: #46b450; transition: width 1000ms ease; will-change: width;"></div> 41 <div id="media-scan-progress-bar" style="position: relative; height: 15px; background: #f0f0f0; border: 1px solid #ccd0d4; border-radius: 4px; overflow: hidden; "> 42 <div id="media-scan-progress-fill" style="height: 100%; width: 0; background: #6366f1; background-size: 200% 100%; transition: width 0.3s ease-in-out; will-change: width;"></div> 43 <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent); background-size: 20px 20px; "></div> 59 44 </div> 60 45 </div> 61 46 47 <!-- Fallback container for scan button when no items exist --> 48 <div class="media-scan-controls" style="display: none; margin: 20px 0; padding: 15px; background: #fff; border: 1px solid #c3c4c7; border-left: 4px solid #6366f1; box-shadow: 0 1px 1px rgba(0,0,0,.04);"></div> 49 62 50 <form method="post"> 63 51 <?php 64 $ unused_media_list->display();52 $media_tracker_unused_media_list->display(); 65 53 ?> 66 54 </form> … … 69 57 <script type="text/javascript"> 70 58 (function($){ 71 // Ensure mediaTracker and ajax _url are defined before using59 // Ensure mediaTracker and ajaxUrl are defined before using 72 60 if (typeof window.mediaTracker === 'undefined') { 73 61 window.mediaTracker = { … … 76 64 }; 77 65 } 78 var AJAX_URL = (window.mediaTracker && window.mediaTracker.ajax_url) || (typeof ajaxurl !== 'undefined' ? ajaxurl : '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>');66 var AJAX_URL = (window.mediaTracker && (window.mediaTracker.ajax_url || window.mediaTracker.ajaxUrl)) || (typeof ajaxurl !== 'undefined' ? ajaxurl : '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>'); 79 67 80 68 var NONCE = (window.mediaTracker && window.mediaTracker.nonce) || '<?php echo esc_js( wp_create_nonce( 'media_tracker_nonce' ) ); ?>'; … … 82 70 var stuckChecks = 0; // consecutive polls stuck near start 83 71 var syncTriggered = false; 72 var clientProgress = 0; // Client-side progress simulation 73 var targetProgress = 0; // Target progress from server 74 var progressAnimInterval = null; // Animation interval 84 75 85 76 function ensureScanButtonExists(label){ … … 100 91 $('.media-scan-controls').hide(); 101 92 } else { 102 $('.media-scan-controls').show().empty().append($newBtn); 103 } 93 // No bulkactions, use fallback container with message 94 var $message = $('<p/>', { 95 html: '<strong>No unused media found.</strong> Click the button below to scan your site for unused media files.', 96 css: { margin: '0 0 12px 0', color: '#646970' } 97 }); 98 $('.media-scan-controls').show().empty().append($message).append($newBtn); 99 } 100 101 // Add Remove All button if it doesn't exist 102 if ($('#remove-all-unused-media').length === 0) { 103 var $removeBtn = $('<button/>', { id: 'remove-all-unused-media', class: 'button button-secondary', text: 'Remove all unused media' }); 104 $removeBtn.css({ marginLeft: '8px' }); 105 $newBtn.after($removeBtn); 106 } 107 104 108 return $newBtn; 105 109 } … … 119 123 $('.media-scan-controls').hide(); 120 124 } else { 121 $('.media-scan-controls').show(); 125 // No bulkactions found (no items), use fallback container 126 $('.media-scan-controls').show().empty().append($btn); 122 127 } 123 128 } … … 140 145 } 141 146 147 // Smooth progress animation function 148 function animateProgress(){ 149 // Gradually move clientProgress towards targetProgress 150 if (clientProgress < targetProgress) { 151 // Increase gradually based on distance 152 var diff = targetProgress - clientProgress; 153 var increment = diff > 20 ? 3 : (diff > 10 ? 2 : 1); // Faster when far, slower when close 154 155 clientProgress += increment; 156 if (clientProgress > targetProgress) { 157 clientProgress = targetProgress; 158 } 159 $('#media-scan-progress-fill').css('width', clientProgress + '%'); 160 } 161 } 162 142 163 function updateProgress(){ 143 164 $.post(AJAX_URL, { action: 'get_media_scan_progress', nonce: NONCE }).done(function(res){ 144 if (!res || !res.success || !res.data) return; 165 if (!res || !res.success || !res.data) { 166 // If no data yet, simulate gradual progress 167 if (targetProgress < 90) { 168 targetProgress += 5; // Add 5% gradually 169 } 170 return; 171 } 172 145 173 var d = res.data; 146 174 var pct = Math.max(0, Math.min(100, parseInt(d.percentage, 10) || 0)); 147 $('#media-scan-progress-fill').css('width', pct + '%'); 175 targetProgress = pct; // Update target from server 176 148 177 $('#media-scan-progress-text').text('Scan status: ' + (d.current_step || '') + ' (' + pct + '%)'); 149 178 … … 152 181 if ((d.step <= 1) && (pct <= 20)) { 153 182 stuckChecks++; 154 if (stuckChecks >= 3) { // ~9s at 3s interval183 if (stuckChecks >= 6) { // Changed from 3 to 6 (now ~3s at 500ms interval) 155 184 syncTriggered = true; 156 185 $.post(AJAX_URL, { action: 'run_media_scan_sync', nonce: NONCE }).always(function(){ … … 164 193 165 194 if (pct >= 100 || d.step >= d.total_steps) { 195 // Stop animations 166 196 clearInterval(pollInterval); 197 clearInterval(progressAnimInterval); 198 clientProgress = 100; 199 targetProgress = 100; 200 $('#media-scan-progress-fill').css('width', '100%'); 201 167 202 // Clear progress transient to avoid sticky progress UI 168 203 $.post(AJAX_URL, { … … 174 209 action: 'get_unused_media_count', 175 210 nonce: NONCE, 176 search: '<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?><?php echo esc_js( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' );?>',177 author_id: '<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?><?php echo esc_js( isset( $_GET['author'] ) ? intval( $_GET['author'] ) : '' );?>'211 search: '<?php echo esc_js( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>', 212 author_id: '<?php echo esc_js( isset( $_GET['author'] ) ? intval( $_GET['author'] ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>' 178 213 }).done(function(countRes){ 179 214 if (countRes && countRes.success && countRes.data) { 180 $('#media-scan-progress-text'). text('Scan complete (100%)- ' + countRes.data.message);215 $('#media-scan-progress-text').html('✅ <strong>Scan Complete!</strong> - ' + countRes.data.message); 181 216 $('#unused-count').text(countRes.data.count); 182 217 } else { 183 $('#media-scan-progress-text'). text('Scan complete(100%)');218 $('#media-scan-progress-text').html('✅ <strong>Scan Complete!</strong> (100%)'); 184 219 } 185 220 }).fail(function(){ 186 $('#media-scan-progress-text'). text('Scan complete(100%)');221 $('#media-scan-progress-text').html('✅ <strong>Scan Complete!</strong> (100%)'); 187 222 }).always(function(){ 188 223 ensureScanButtonExists('Rescan').prop('disabled', false); 189 224 // refresh list table so item count and pagination match new cache 190 225 refreshListTable('Rescan'); 191 // keep progress bar visible for 15 seconds, then hide226 // keep progress bar visible for 3 seconds, then reload page 192 227 setTimeout(function(){ 193 $('#media-scan-progress').hide(); 194 }, 15000); 228 $('#media-scan-progress').fadeOut(300, function(){ 229 // Reload page to show fresh data 230 location.reload(); 231 }); 232 }, 3000); // Reduced from 15000 to 3000 195 233 }); 196 234 }); … … 201 239 function startPolling(){ 202 240 if (pollInterval) { clearInterval(pollInterval); } 203 pollInterval = setInterval(updateProgress, 1000); 241 if (progressAnimInterval) { clearInterval(progressAnimInterval); } 242 243 // Start smooth animation interval (runs every 50ms) 244 progressAnimInterval = setInterval(animateProgress, 50); 245 246 // Start server polling (every 500ms) 247 pollInterval = setInterval(updateProgress, 500); 204 248 updateProgress(); 205 249 } … … 207 251 $(document).on('click', '#run-media-scan', function(e){ 208 252 e.preventDefault(); 253 e.stopPropagation(); 254 209 255 var $btn = $(this); 210 $('#media-scan-progress').show(); 256 257 // Prevent double clicks 258 if ($btn.prop('disabled')) { 259 return false; 260 } 261 262 // Reset progress variables 263 clientProgress = 0; 264 targetProgress = 5; // Start with 5% 265 syncTriggered = false; 266 stuckChecks = 0; 267 268 // Show progress bar with animation 269 var $progress = $('#media-scan-progress'); 270 $progress.css('display', 'block').hide().fadeIn(300); 271 211 272 $('#media-scan-progress-fill').css('width', '0%'); 212 273 $('#media-scan-progress-text').text('Scan status: Starting... (0%)'); … … 218 279 }).done(function(res){ 219 280 $btn.text('Scanning...'); 281 282 // Start smooth animation immediately 283 clientProgress = 0; 284 targetProgress = 10; // Jump to 10% quickly 285 animateProgress(); 286 220 287 startPolling(); 221 }).fail(function(){ 222 $('#media-scan-progress').hide(); 288 }).fail(function(xhr, status, error){ 289 $('#media-scan-progress-text').html('❌ <strong>Error:</strong> ' + (xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Failed to start scan. Please refresh and try again.')); 290 setTimeout(function(){ 291 $('#media-scan-progress').fadeOut(300); 292 }, 3000); 223 293 ensureScanButtonExists('Scan Unused Media').prop('disabled', false); 224 294 }); 295 296 return false; 297 }); 298 299 // Handle Remove All Unused Media button click 300 $(document).on('click', '#remove-all-unused-media', function(e){ 301 e.preventDefault(); 302 e.stopPropagation(); 303 304 var $btn = $(this); 305 306 // Prevent double clicks 307 if ($btn.prop('disabled')) { 308 return false; 309 } 310 311 // Show confirmation alert 312 if (!confirm('Are you sure you want to delete all unused media? This action cannot be undone. Continue?')) { 313 return false; 314 } 315 316 $btn.prop('disabled', true).text('Deleting...'); 317 318 // Show progress bar 319 var $progress = $('#media-scan-progress'); 320 $progress.css('display', 'block').hide().fadeIn(300); 321 $('#media-scan-progress-text').text('Deleting all unused media...'); 322 $('#media-scan-progress-fill').css('width', '0%'); 323 324 $.post(AJAX_URL, { 325 action: 'remove_all_unused_media', 326 nonce: NONCE 327 }).done(function(res){ 328 if (res && res.success) { 329 $('#media-scan-progress-text').html('✅ <strong>Success!</strong> - ' + (res.data.message || 'All unused media has been deleted.')); 330 $('#media-scan-progress-fill').css('width', '100%'); 331 $('#unused-count').text('0'); 332 333 // Refresh the list after 2 seconds 334 setTimeout(function(){ 335 location.reload(); 336 }, 2000); 337 } else { 338 throw new Error(res.data && res.data.message || 'Unknown error'); 339 } 340 }).fail(function(xhr, status, error){ 341 var errorMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Failed to delete unused media.'; 342 $('#media-scan-progress-text').html('❌ <strong>Error:</strong> ' + errorMsg); 343 setTimeout(function(){ 344 $('#media-scan-progress').fadeOut(300); 345 }, 3000); 346 $btn.prop('disabled', false).text('Remove all unused media'); 347 }); 348 349 return false; 225 350 }); 226 351 -
media-tracker/trunk/includes/Installer.php
r3441211 r3454648 21 21 $this->add_version(); 22 22 $this->optimize_database_indexes(); 23 $this->schedule_cron_jobs(); 24 } 25 26 /** 27 * Schedule cron jobs for background processing 28 * 29 * @since 1.2.3 30 * @access public 31 * @param none 32 * @return void 33 */ 34 public function schedule_cron_jobs() { 35 // Schedule the batch processing cron job 36 if (!wp_next_scheduled('media_tracker_batch_process')) { 37 // Prefer the faster schedule when available 38 $interval = 'five_minutes'; 39 $schedules = wp_get_schedules(); 40 if (!isset($schedules[$interval])) { 41 $interval = 'hourly'; 42 } 43 wp_schedule_event(time(), $interval, 'media_tracker_batch_process'); 44 } 45 } 46 47 /** 48 * Clear scheduled cron jobs on deactivation 49 * 50 * @since 1.2.3 51 * @access public 52 * @param none 53 * @return void 54 */ 55 public static function clear_cron_jobs() { 56 // Clear the batch processing cron job 57 $timestamp = wp_next_scheduled('media_tracker_batch_process'); 58 if ($timestamp) { 59 wp_unschedule_event($timestamp, 'media_tracker_batch_process'); 60 } 61 62 // Also clear any scheduled hooks that might have been set 63 wp_clear_scheduled_hook('media_tracker_batch_process'); 23 64 } 24 65 -
media-tracker/trunk/languages/media-tracker.pot
r3432010 r3454648 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Media Tracker 1. 2.1\n"5 "Project-Id-Version: Media Tracker 1.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/media-tracker\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-0 1-04T08:21:49+00:00\n"12 "POT-Creation-Date: 2026-02-05T12:57:27+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 17 17 #. Plugin Name of the plugin 18 18 #: media-tracker.php 19 #: includes/Admin/Menu.php:104 20 #: includes/Admin/Menu.php:105 19 21 msgid "Media Tracker" 20 22 msgstr "" … … 35 37 msgstr "" 36 38 37 #: includes/Admin/Duplicate_Images.php:32 38 msgid "Every 5 Minutes" 39 msgstr "" 40 41 #: includes/Admin/Duplicate_Images.php:64 39 #: includes/Admin/Duplicate_Images.php:43 42 40 msgid "Duplicates" 43 41 msgstr "" 44 42 45 #: includes/Admin/Duplicate_Images.php: 6643 #: includes/Admin/Duplicate_Images.php:45 46 44 msgid "Duplicate images filter" 47 45 msgstr "" 48 46 49 #: includes/Admin/Duplicate_Images.php: 6747 #: includes/Admin/Duplicate_Images.php:46 50 48 msgid "All Media" 51 49 msgstr "" 52 50 53 #: includes/Admin/Duplicate_Images.php: 6951 #: includes/Admin/Duplicate_Images.php:48 54 52 msgid "Show Duplicate Images" 55 53 msgstr "" 56 54 57 #: includes/Admin/Duplicate_Images.php: 7455 #: includes/Admin/Duplicate_Images.php:53 58 56 msgid "Re-scan" 59 57 msgstr "" 60 58 61 #: includes/Admin/Duplicate_Images.php:380 62 #: includes/Admin/Duplicate_Images.php:589 63 #: includes/Admin/Menu.php:124 64 #: includes/Admin/Menu.php:143 65 #: includes/Admin/Menu.php:180 66 #: includes/Admin/Menu.php:216 67 #: includes/Admin/Menu.php:307 68 #: includes/Admin/Menu.php:323 59 #: includes/Admin/Duplicate_Images.php:376 60 #: includes/Admin/Duplicate_Images.php:564 61 #: includes/Admin/Duplicate_Images.php:611 62 #: includes/Admin/Menu.php:118 69 63 msgid "Unauthorized" 70 64 msgstr "" 71 65 72 66 #. translators: 1: number of hashes reset, 2: number of images being rescanned 73 #: includes/Admin/Duplicate_Images.php:6 1667 #: includes/Admin/Duplicate_Images.php:600 74 68 #, php-format 75 69 msgid "Reset %1$d hashes. Re-scanning %2$d images..." 76 70 msgstr "" 77 71 78 #: includes/Admin/Media_Usage.php:36 72 #: includes/Admin/Duplicate_Images.php:617 73 msgid "No images selected." 74 msgstr "" 75 76 #. translators: %d: number of deleted images 77 #: includes/Admin/Duplicate_Images.php:643 78 #, php-format 79 msgid "Deleted %d duplicate images." 80 msgstr "" 81 82 #: includes/Admin/Duplicate_Images.php:649 83 msgid "No images were deleted." 84 msgstr "" 85 86 #: includes/Admin/Duplicate_Images.php:684 87 msgid "items" 88 msgstr "" 89 90 #: includes/Admin/Media_Usage.php:178 79 91 msgid "Media Usage" 80 92 msgstr "" 81 93 82 #: includes/Admin/Media_Usage.php:51 94 #: includes/Admin/Media_Usage.php:193 95 #: includes/Admin/views/tabs/tab-duplicates.php:151 83 96 msgid "Usages Count" 84 97 msgstr "" 85 98 86 #: includes/Admin/Media_Usage.php: 7399 #: includes/Admin/Media_Usage.php:215 87 100 msgid "Open attachment edit screen" 88 101 msgstr "" 89 102 90 #: includes/Admin/Media_Usage.php: 172103 #: includes/Admin/Media_Usage.php:314 91 104 msgid "#" 92 105 msgstr "" 93 106 94 #: includes/Admin/Media_Usage.php:173 107 #: includes/Admin/Media_Usage.php:315 108 #: includes/Admin/views/tabs/tab-duplicates.php:136 95 109 msgid "Title" 96 110 msgstr "" 97 111 98 #: includes/Admin/Media_Usage.php:174 112 #: includes/Admin/Media_Usage.php:316 113 #: includes/Admin/views/tabs/tab-overview.php:304 114 #: includes/Admin/views/tabs/tab-overview.php:354 99 115 msgid "Type" 100 116 msgstr "" 101 117 102 #: includes/Admin/Media_Usage.php: 175118 #: includes/Admin/Media_Usage.php:317 103 119 msgid "Date Added" 104 120 msgstr "" 105 121 106 #: includes/Admin/Media_Usage.php:176 122 #: includes/Admin/Media_Usage.php:318 123 #: includes/Admin/views/tabs/tab-duplicates.php:158 124 #: includes/Admin/views/tabs/tab-overview.php:307 125 #: includes/Admin/views/tabs/tab-overview.php:356 107 126 msgid "Actions" 108 127 msgstr "" 109 128 110 #: includes/Admin/Media_Usage.php: 189129 #: includes/Admin/Media_Usage.php:331 111 130 msgid "System Setting" 112 131 msgstr "" 113 132 114 #: includes/Admin/Media_Usage.php: 192133 #: includes/Admin/Media_Usage.php:334 115 134 msgid "Customize Site Icon" 116 135 msgstr "" 117 136 118 137 #. Translators: This is a time difference string 119 #: includes/Admin/Media_Usage.php: 202138 #: includes/Admin/Media_Usage.php:344 120 139 #, php-format 121 140 msgid "%s ago" 122 141 msgstr "" 123 142 124 #: includes/Admin/Media_Usage.php: 224143 #: includes/Admin/Media_Usage.php:366 125 144 msgid "Admin View" 126 145 msgstr "" 127 146 128 #: includes/Admin/Media_Usage.php: 225147 #: includes/Admin/Media_Usage.php:367 129 148 msgid "Frontend View" 130 149 msgstr "" 131 150 132 #: includes/Admin/Media_Usage.php: 233151 #: includes/Admin/Media_Usage.php:375 133 152 msgid "No posts or pages found using this media file." 134 153 msgstr "" 135 154 136 #: includes/Admin/Media_Usage.php: 274155 #: includes/Admin/Media_Usage.php:416 137 156 msgid "Site Icon (Favicon)" 138 157 msgstr "" 139 158 140 #: includes/Admin/Menu.php:52 141 #: includes/Admin/Menu.php:53 142 msgid "Unused Media" 143 msgstr "" 144 145 #: includes/Admin/Menu.php:108 146 msgid "Unused Media per page" 147 msgstr "" 148 149 #: includes/Admin/Menu.php:135 159 #: includes/Admin/Menu.php:129 150 160 msgid "Cache cleared successfully." 151 161 msgstr "" 152 162 153 #: includes/Admin/Menu.php:207 163 #: includes/Admin/Menu.php:137 164 #: includes/Admin/Menu.php:178 165 #: includes/Admin/Menu.php:217 166 #: includes/Admin/Menu.php:325 167 #: includes/Admin/Menu.php:347 168 #: includes/Admin/Menu.php:379 169 msgid "Unauthorized: You do not have permission to perform this action." 170 msgstr "" 171 172 #: includes/Admin/Menu.php:142 173 #: includes/Admin/Menu.php:330 174 #: includes/Admin/Menu.php:352 175 #: includes/Admin/Menu.php:384 176 msgid "Security check failed." 177 msgstr "" 178 179 #: includes/Admin/Menu.php:183 180 #: includes/Admin/Menu.php:222 181 msgid "Security check failed. Please refresh the page and try again." 182 msgstr "" 183 184 #: includes/Admin/Menu.php:208 154 185 msgid "Scan started." 155 186 msgstr "" 156 187 157 #: includes/Admin/Menu.php:2 51188 #: includes/Admin/Menu.php:262 158 189 msgid "Scan completed." 159 190 msgstr "" 160 191 161 #: includes/Admin/Menu.php:3 15192 #: includes/Admin/Menu.php:339 162 193 msgid "Progress cleared." 163 194 msgstr "" 164 195 165 196 #. translators: %d: number of unused media found! 166 #: includes/Admin/Menu.php:3 41197 #: includes/Admin/Menu.php:368 167 198 #, php-format 168 199 msgid "%d unused image found" … … 171 202 msgstr[1] "" 172 203 173 #: includes/Admin/Unused_Media_List.php:67 204 #. translators: %d: number of items deleted 205 #: includes/Admin/Menu.php:413 206 #, php-format 207 msgid "Successfully deleted %d unused media item." 208 msgid_plural "Successfully deleted %d unused media items." 209 msgstr[0] "" 210 msgstr[1] "" 211 212 #: includes/Admin/Menu.php:421 213 msgid "No unused media items found or failed to delete." 214 msgstr "" 215 216 #: includes/Admin/PluginMeta.php:61 217 msgid "Settings" 218 msgstr "" 219 220 #: includes/Admin/Unused_Media_List.php:54 174 221 msgid "File" 175 222 msgstr "" 176 223 177 #: includes/Admin/Unused_Media_List.php: 68224 #: includes/Admin/Unused_Media_List.php:55 178 225 msgid "Author" 179 226 msgstr "" 180 227 181 #: includes/Admin/Unused_Media_List.php:69 228 #: includes/Admin/Unused_Media_List.php:56 229 #: includes/Admin/views/tabs/tab-duplicates.php:137 230 #: includes/Admin/views/tabs/tab-overview.php:305 182 231 msgid "Size" 183 232 msgstr "" 184 233 185 #: includes/Admin/Unused_Media_List.php:70 234 #: includes/Admin/Unused_Media_List.php:57 235 #: includes/Admin/views/tabs/tab-overview.php:306 186 236 msgid "Date" 187 237 msgstr "" 188 238 189 #: includes/Admin/Unused_Media_List.php: 95239 #: includes/Admin/Unused_Media_List.php:82 190 240 msgid "Edit" 191 241 msgstr "" 192 242 193 #: includes/Admin/Unused_Media_List.php:96 243 #: includes/Admin/Unused_Media_List.php:83 244 #: includes/Admin/views/tabs/tab-overview.php:329 245 #: includes/Admin/views/tabs/tab-overview.php:380 194 246 msgid "View" 195 247 msgstr "" 196 248 249 #: includes/Admin/Unused_Media_List.php:84 250 msgid "Delete Permanently" 251 msgstr "" 252 253 #. translators: %s: post title 254 #: includes/Admin/Unused_Media_List.php:88 255 #, php-format 256 msgid "\"%s\" (Edit)" 257 msgstr "" 258 197 259 #: includes/Admin/Unused_Media_List.php:97 198 msgid "Delete Permanently"199 msgstr ""200 201 #. translators: %s: post title202 #: includes/Admin/Unused_Media_List.php:101203 #, php-format204 msgid "\"%s\" (Edit)"205 msgstr ""206 207 #: includes/Admin/Unused_Media_List.php:110208 260 msgid "File name:" 209 261 msgstr "" 210 262 211 #: includes/Admin/Unused_Media_List.php:10 94263 #: includes/Admin/Unused_Media_List.php:1019 212 264 msgid "Delete permanently" 213 265 msgstr "" 214 266 215 267 #. translators: %d: number of deleted media files 216 #: includes/Admin/Unused_Media_List.php:1 119268 #: includes/Admin/Unused_Media_List.php:1044 217 269 #, php-format 218 270 msgid "%d media file(s) deleted successfully." 219 271 msgstr "" 220 272 221 #: includes/Admin/views/unused-media-list.php:22 222 msgid "Unused Media Files" 223 msgstr "" 224 225 #: includes/Admin/views/unused-media-list.php:35 226 msgid "Unused Media Found!" 227 msgstr "" 228 229 #: includes/Admin/views/unused-media-list.php:44 230 msgid "Rescan" 231 msgstr "" 232 233 #: includes/Admin/views/unused-media-list.php:44 234 msgid "Run Scan in Background" 235 msgstr "" 236 237 #: includes/Admin/views/unused-media-list.php:49 238 msgid "Search Media" 239 msgstr "" 240 241 #: includes/Admin/views/unused-media-list.php:55 273 #: includes/Admin/views/media-tracker.php:7 274 msgid "MediaTracker" 275 msgstr "" 276 277 #. translators: %s: Plugin version number. 278 #: includes/Admin/views/media-tracker.php:15 279 #, php-format 280 msgid "Version %s" 281 msgstr "" 282 283 #: includes/Admin/views/media-tracker.php:61 284 msgid "Add New Connection" 285 msgstr "" 286 287 #: includes/Admin/views/media-tracker.php:63 288 msgid "×" 289 msgstr "" 290 291 #: includes/Admin/views/media-tracker.php:67 292 msgid "Connection Name" 293 msgstr "" 294 295 #: includes/Admin/views/media-tracker.php:68 296 msgid "My S3 Backup" 297 msgstr "" 298 299 #: includes/Admin/views/media-tracker.php:71 300 msgid "Provider" 301 msgstr "" 302 303 #: includes/Admin/views/media-tracker.php:73 304 msgid "Google Drive" 305 msgstr "" 306 307 #: includes/Admin/views/media-tracker.php:74 308 msgid "Amazon S3" 309 msgstr "" 310 311 #: includes/Admin/views/media-tracker.php:75 312 msgid "Dropbox" 313 msgstr "" 314 315 #: includes/Admin/views/media-tracker.php:79 316 msgid "Root Folder / Bucket" 317 msgstr "" 318 319 #: includes/Admin/views/media-tracker.php:80 320 msgid "/MediaTrackerPro/backup or media-tracker-pro" 321 msgstr "" 322 323 #: includes/Admin/views/media-tracker.php:83 324 msgid "Region / Location" 325 msgstr "" 326 327 #: includes/Admin/views/media-tracker.php:84 328 msgid "us-east-1, europe-west1 etc." 329 msgstr "" 330 331 #: includes/Admin/views/media-tracker.php:86 332 msgid "Production credentials (Access Key, Secret Key) are handled via the WordPress settings page. This modal only previews UI and flow." 333 msgstr "" 334 335 #: includes/Admin/views/media-tracker.php:89 336 msgid "Cancel" 337 msgstr "" 338 339 #: includes/Admin/views/media-tracker.php:90 340 msgid "Test Connection" 341 msgstr "" 342 343 #: includes/Admin/views/media-tracker.php:91 344 msgid "Save Connection" 345 msgstr "" 346 347 #: includes/Admin/views/tabs/tab-documents.php:15 348 #: includes/functions.php:82 349 msgid "Documents" 350 msgstr "" 351 352 #: includes/Admin/views/tabs/tab-documents.php:17 353 msgid "Manage and track your document files." 354 msgstr "" 355 356 #: includes/Admin/views/tabs/tab-documents.php:29 357 msgid "Documentation" 358 msgstr "" 359 360 #: includes/Admin/views/tabs/tab-documents.php:31 361 msgid "Learn how to track media usage and clean up unused files in WordPress." 362 msgstr "" 363 364 #: includes/Admin/views/tabs/tab-documents.php:35 365 msgid "Read More" 366 msgstr "" 367 368 #: includes/Admin/views/tabs/tab-documents.php:44 369 msgid "Join Our Community" 370 msgstr "" 371 372 #: includes/Admin/views/tabs/tab-documents.php:46 373 msgid "Join our community to discuss features, share ideas, and get help from other users." 374 msgstr "" 375 376 #: includes/Admin/views/tabs/tab-documents.php:49 377 msgid "Join Now" 378 msgstr "" 379 380 #: includes/Admin/views/tabs/tab-documents.php:58 381 msgid "Show your love" 382 msgstr "" 383 384 #: includes/Admin/views/tabs/tab-documents.php:60 385 msgid "Enjoying Media Tracker? Please rate us on WordPress.org to help us grow." 386 msgstr "" 387 388 #: includes/Admin/views/tabs/tab-documents.php:63 389 msgid "Rate Us" 390 msgstr "" 391 392 #: includes/Admin/views/tabs/tab-documents.php:71 393 msgid "Video Tutorials" 394 msgstr "" 395 396 #: includes/Admin/views/tabs/tab-documents.php:77 397 #: includes/Admin/views/tabs/tab-documents.php:101 398 msgid "Getting Started" 399 msgstr "" 400 401 #: includes/Admin/views/tabs/tab-documents.php:87 402 msgid "Getting Started with Media Tracker" 403 msgstr "" 404 405 #: includes/Admin/views/tabs/tab-documents.php:90 406 msgid "Learn the basics of Media Tracker in 2 minutes. Watch our comprehensive guide to master file management." 407 msgstr "" 408 409 #: includes/Admin/views/tabs/tab-duplicates.php:90 410 #: includes/functions.php:42 411 msgid "Duplicate Media" 412 msgstr "" 413 414 #: includes/Admin/views/tabs/tab-duplicates.php:92 415 msgid "Same hash, probable duplicate images grouped together. Use delete to remove selected images." 416 msgstr "" 417 418 #: includes/Admin/views/tabs/tab-duplicates.php:125 419 msgid "Scan Duplicates" 420 msgstr "" 421 422 #: includes/Admin/views/tabs/tab-duplicates.php:135 423 msgid "Thumbnail" 424 msgstr "" 425 426 #: includes/Admin/views/tabs/tab-duplicates.php:196 427 #: includes/Admin/views/tabs/tab-overview.php:249 428 msgid "Delete" 429 msgstr "" 430 431 #: includes/Admin/views/tabs/tab-duplicates.php:204 432 msgid "Delete Selected" 433 msgstr "" 434 435 #: includes/Admin/views/tabs/tab-duplicates.php:211 436 msgid "No duplicate images found." 437 msgstr "" 438 439 #: includes/Admin/views/tabs/tab-duplicates.php:214 440 msgid "Duplicate images handler not available." 441 msgstr "" 442 443 #: includes/Admin/views/tabs/tab-external-storage.php:18 444 msgid "Cloud Storage & Automatic Offloading" 445 msgstr "" 446 447 #: includes/Admin/views/tabs/tab-external-storage.php:19 448 msgid "Upgrade to Media Tracker Pro to automatically move and archive unused files to S3, Google Drive, or Dropbox." 449 msgstr "" 450 451 #: includes/Admin/views/tabs/tab-external-storage.php:21 452 msgid "Connect Google Drive, S3, Dropbox" 453 msgstr "" 454 455 #: includes/Admin/views/tabs/tab-external-storage.php:22 456 msgid "Auto-offload unused media" 457 msgstr "" 458 459 #: includes/Admin/views/tabs/tab-external-storage.php:23 460 msgid "Archive old media automatically" 461 msgstr "" 462 463 #: includes/Admin/views/tabs/tab-external-storage.php:24 464 msgid "Backup before permanent delete" 465 msgstr "" 466 467 #: includes/Admin/views/tabs/tab-external-storage.php:25 468 msgid "Storage limit alerts" 469 msgstr "" 470 471 #: includes/Admin/views/tabs/tab-external-storage.php:26 472 msgid "Restore from cloud anytime" 473 msgstr "" 474 475 #: includes/Admin/views/tabs/tab-multisite.php:18 476 msgid "Multi-site Network Management" 477 msgstr "" 478 479 #: includes/Admin/views/tabs/tab-multisite.php:19 480 msgid "Upgrade to Media Tracker Pro to manage and track media from multiple WordPress sites in one central dashboard." 481 msgstr "" 482 483 #: includes/Admin/views/tabs/tab-multisite.php:21 484 msgid "Connect unlimited network sites" 485 msgstr "" 486 487 #: includes/Admin/views/tabs/tab-multisite.php:22 488 msgid "Network-wide media tracking" 489 msgstr "" 490 491 #: includes/Admin/views/tabs/tab-multisite.php:23 492 msgid "Cross-site duplicate detection" 493 msgstr "" 494 495 #: includes/Admin/views/tabs/tab-multisite.php:24 496 msgid "Centralized storage overview" 497 msgstr "" 498 499 #: includes/Admin/views/tabs/tab-multisite.php:25 500 msgid "Per-site cleanup rules" 501 msgstr "" 502 503 #: includes/Admin/views/tabs/tab-multisite.php:26 504 msgid "Network admin controls" 505 msgstr "" 506 507 #: includes/Admin/views/tabs/tab-optimization.php:18 508 msgid "Advanced Image Optimization" 509 msgstr "" 510 511 #: includes/Admin/views/tabs/tab-optimization.php:19 512 msgid "Upgrade to Media Tracker Pro to access powerful optimization tools including compression, WebP conversion, and lazy loading." 513 msgstr "" 514 515 #: includes/Admin/views/tabs/tab-optimization.php:21 516 msgid "Lossless image compression" 517 msgstr "" 518 519 #: includes/Admin/views/tabs/tab-optimization.php:22 520 msgid "Automatic WebP conversion" 521 msgstr "" 522 523 #: includes/Admin/views/tabs/tab-optimization.php:23 524 msgid "Smart lazy loading" 525 msgstr "" 526 527 #: includes/Admin/views/tabs/tab-optimization.php:24 528 msgid "Bulk optimization queue" 529 msgstr "" 530 531 #: includes/Admin/views/tabs/tab-optimization.php:25 532 msgid "Fallback for old browsers" 533 msgstr "" 534 535 #: includes/Admin/views/tabs/tab-optimization.php:26 536 msgid "Quality control settings" 537 msgstr "" 538 539 #: includes/Admin/views/tabs/tab-overview.php:145 540 #: includes/functions.php:26 541 msgid "Dashboard" 542 msgstr "" 543 544 #: includes/Admin/views/tabs/tab-overview.php:147 545 msgid "Manage unused media, duplicate, optimization and cloud offload from a clean dashboard with Media Tracker." 546 msgstr "" 547 548 #: includes/Admin/views/tabs/tab-overview.php:157 549 #: includes/Admin/views/unused-media-list.php:21 550 #: includes/functions.php:34 551 msgid "Unused Media" 552 msgstr "" 553 554 #. translators: %d: Number of unused files. 555 #. translators: %d: Number of duplicate files. 556 #. translators: %d: Total number of media files. 557 #: includes/Admin/views/tabs/tab-overview.php:162 558 #: includes/Admin/views/tabs/tab-overview.php:187 559 #: includes/Admin/views/tabs/tab-overview.php:204 560 #, php-format 561 msgid "%d Files" 562 msgstr "" 563 564 #. translators: %s: Formatted file size (e.g. 1.5 MB). 565 #: includes/Admin/views/tabs/tab-overview.php:170 566 #, php-format 567 msgid "Potential saving: %s" 568 msgstr "" 569 570 #: includes/Admin/views/tabs/tab-overview.php:179 571 msgid "Duplicates Found" 572 msgstr "" 573 574 #: includes/Admin/views/tabs/tab-overview.php:184 575 msgid "Scan Required" 576 msgstr "" 577 578 #: includes/Admin/views/tabs/tab-overview.php:192 579 msgid "Based on file hash matching" 580 msgstr "" 581 582 #: includes/Admin/views/tabs/tab-overview.php:199 583 msgid "Total Media" 584 msgstr "" 585 586 #: includes/Admin/views/tabs/tab-overview.php:207 587 msgid "Total files in library" 588 msgstr "" 589 590 #: includes/Admin/views/tabs/tab-overview.php:215 591 msgid "Quick Actions" 592 msgstr "" 593 594 #: includes/Admin/views/tabs/tab-overview.php:220 595 msgid "Scan for Unused Media" 596 msgstr "" 597 598 #: includes/Admin/views/tabs/tab-overview.php:221 599 msgid "Scan all content to find unused media files." 600 msgstr "" 601 602 #: includes/Admin/views/tabs/tab-overview.php:224 603 msgid "Scan" 604 msgstr "" 605 606 #: includes/Admin/views/tabs/tab-overview.php:230 607 msgid "Find Duplicates" 608 msgstr "" 609 610 #: includes/Admin/views/tabs/tab-overview.php:231 611 msgid "Detects duplicate images using file hash matching." 612 msgstr "" 613 614 #: includes/Admin/views/tabs/tab-overview.php:234 615 msgid "Find" 616 msgstr "" 617 618 #: includes/Admin/views/tabs/tab-overview.php:240 619 msgid "Bulk Delete Unused" 620 msgstr "" 621 622 #. translators: %d: Number of unused files. 623 #: includes/Admin/views/tabs/tab-overview.php:244 624 #, php-format 625 msgid "%d unused files found. Delete safely after backup." 626 msgstr "" 627 628 #: includes/Admin/views/tabs/tab-overview.php:257 629 msgid "Media Statistics" 630 msgstr "" 631 632 #. translators: %d: Number of files for a specific mime type. 633 #: includes/Admin/views/tabs/tab-overview.php:279 634 #, php-format 635 msgid "%d files" 636 msgstr "" 637 638 #: includes/Admin/views/tabs/tab-overview.php:287 639 msgid "No media files found yet." 640 msgstr "" 641 642 #: includes/Admin/views/tabs/tab-overview.php:297 643 msgid "Recent Unused Media" 644 msgstr "" 645 646 #: includes/Admin/views/tabs/tab-overview.php:303 647 #: includes/Admin/views/tabs/tab-overview.php:353 648 msgid "File Name" 649 msgstr "" 650 651 #: includes/Admin/views/tabs/tab-overview.php:339 652 msgid "No unused media found! Great job keeping your library clean." 653 msgstr "" 654 655 #: includes/Admin/views/tabs/tab-overview.php:347 656 msgid "Most Used Media" 657 msgstr "" 658 659 #: includes/Admin/views/tabs/tab-overview.php:355 660 msgid "Usage Count" 661 msgstr "" 662 663 #. translators: %d: Number of times the media is used. 664 #: includes/Admin/views/tabs/tab-overview.php:373 665 #, php-format 666 msgid "%d times" 667 msgstr "" 668 669 #: includes/Admin/views/tabs/tab-overview.php:390 670 msgid "No media usage data available yet." 671 msgstr "" 672 673 #: includes/Admin/views/tabs/tab-security.php:18 674 msgid "Security & Activity Logs" 675 msgstr "" 676 677 #: includes/Admin/views/tabs/tab-security.php:19 678 msgid "Upgrade to Media Tracker Pro for role-based permissions, activity logging, and complete audit trails for all sensitive actions." 679 msgstr "" 680 681 #: includes/Admin/views/tabs/tab-security.php:21 682 msgid "Role-based access control" 683 msgstr "" 684 685 #: includes/Admin/views/tabs/tab-security.php:22 686 msgid "Full activity audit logs" 687 msgstr "" 688 689 #: includes/Admin/views/tabs/tab-security.php:23 690 msgid "Bulk delete confirmation" 691 msgstr "" 692 693 #: includes/Admin/views/tabs/tab-security.php:24 694 msgid "File whitelist protection" 695 msgstr "" 696 697 #: includes/Admin/views/tabs/tab-security.php:25 698 msgid "Export logs to CSV" 699 msgstr "" 700 701 #: includes/Admin/views/tabs/tab-security.php:26 702 msgid "User action tracking" 703 msgstr "" 704 705 #: includes/Admin/views/tabs/tab-unused-media.php:51 706 msgid "Unused media list class not found." 707 msgstr "" 708 709 #: includes/Admin/views/unused-media-list.php:23 710 msgid "Detect media files not used anywhere on your site for safe cleanup." 711 msgstr "" 712 713 #: includes/Admin/views/unused-media-list.php:33 714 msgid "unused media found!" 715 msgstr "" 716 717 #: includes/Admin/views/unused-media-list.php:39 242 718 msgid "Scan status: Ready to scan..." 243 719 msgstr "" 244 720 245 #: includes/Installer.php:104 721 #: includes/Assets.php:38 722 msgid "Image hashes will be refreshed and all images will be re-scanned. Continue?" 723 msgstr "" 724 725 #: includes/Assets.php:39 726 msgid "Re-scanning..." 727 msgstr "" 728 729 #: includes/Assets.php:40 730 msgid "Error re-scanning images" 731 msgstr "" 732 733 #: includes/Cron_Schedules.php:30 734 msgid "Every 5 Minutes" 735 msgstr "" 736 737 #: includes/functions.php:50 738 msgid "External Storage" 739 msgstr "" 740 741 #: includes/functions.php:58 742 msgid "Optimization" 743 msgstr "" 744 745 #: includes/functions.php:65 746 msgid "Security & Logs" 747 msgstr "" 748 749 #: includes/functions.php:73 750 msgid "Multi-site" 751 msgstr "" 752 753 #: includes/functions.php:90 754 msgid "Go Pro" 755 msgstr "" 756 757 #. translators: %s template file path 758 #: includes/functions.php:222 759 #, php-format 760 msgid "%s does not exist." 761 msgstr "" 762 763 #: includes/functions.php:406 764 msgid "This feature is available in Media Tracker Pro." 765 msgstr "" 766 767 #: includes/functions.php:425 768 msgid "Upgrade to Media Tracker Pro" 769 msgstr "" 770 771 #: includes/Installer.php:144 246 772 msgid "Media Tracker Plugin Feedback" 247 773 msgstr "" 248 774 249 #: includes/Installer.php:1 22775 #: includes/Installer.php:162 250 776 msgid "If you have a moment, we'd love to know why you're deactivating the Media Tracker plugin!" 251 777 msgstr "" 252 778 253 #: includes/Installer.php:1 26779 #: includes/Installer.php:166 254 780 msgid "Enter your feedback here..." 255 781 msgstr "" 256 782 257 #: includes/Installer.php:1 30783 #: includes/Installer.php:170 258 784 msgid "Skip & Deactivate" 259 785 msgstr "" 260 786 261 #: includes/Installer.php:1 31787 #: includes/Installer.php:171 262 788 msgid "Submit & Deactivate" 263 789 msgstr "" 264 265 #: media-tracker.php:110266 msgid "Image hashes will be refreshed and all images will be re-scanned. Continue?"267 msgstr ""268 269 #: media-tracker.php:111270 msgid "Re-scanning..."271 msgstr ""272 273 #: media-tracker.php:112274 msgid "Error re-scanning images"275 msgstr "" -
media-tracker/trunk/media-tracker.php
r3441211 r3454648 5 5 * Author: TheBitCraft 6 6 * Author URI: https://thebitcraft.com/ 7 * Version: 1. 2.27 * Version: 1.3.0 8 8 * Requires PHP: 7.4 9 9 * Requires at least: 5.9 … … 28 28 * @var string 29 29 */ 30 const version = '1. 2.2';30 const version = '1.3.0'; 31 31 32 32 /** … … 36 36 $this->define_constants(); 37 37 register_activation_hook( __FILE__, array( $this, 'activate' ) ); 38 register_deactivation_hook( __FILE__, array( '\Media_Tracker\Installer', 'clear_cron_jobs' ) ); 38 39 add_action( 'plugins_loaded', array( $this, 'init_plugin' ) ); 39 add_action( 'admin_enqueue_scripts', array( $this, 'admin_script' ) );40 40 add_action( 'wp_ajax_mt_save_feedback', array( '\Media_Tracker\Installer', 'save_feedback' ) ); 41 41 add_action( 'current_screen', function( $screen ) { … … 91 91 public function init_plugin() { 92 92 new Media_Tracker\Media_Tracker_i18n(); 93 94 if ( is_admin() ) { 93 new Media_Tracker\Assets(); 94 new Media_Tracker\Cron_Schedules(); 95 // Load Admin class in admin dashboard OR during cron execution 96 if ( is_admin() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { 95 97 new Media_Tracker\Admin(); 96 98 } 97 }98 99 /**100 * Register necessary CSS and JS101 * @ Admin102 */103 public function admin_script() {104 wp_enqueue_style( 'mt-admin-style', MEDIA_TRACKER_URL . '/assets/dist/css/mt-admin.css', false, MEDIA_TRACKER_VERSION );105 wp_enqueue_script( 'mt-admin-script', MEDIA_TRACKER_URL . '/assets/dist/js/mt-admin.js', array( 'jquery' ), MEDIA_TRACKER_VERSION, true );106 wp_localize_script( 'mt-admin-script', 'mediaTracker', array(107 'ajax_url' => admin_url( 'admin-ajax.php' ),108 'nonce' => wp_create_nonce( 'media_tracker_nonce' ),109 'i18n' => array(110 'rescan_confirm' => __( 'Image hashes will be refreshed and all images will be re-scanned. Continue?', 'media-tracker' ),111 'rescanning' => __( 'Re-scanning...', 'media-tracker' ),112 'rescan_error' => __( 'Error re-scanning images', 'media-tracker' ),113 ),114 ) );115 99 } 116 100 } -
media-tracker/trunk/readme.txt
r3441211 r3454648 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.4 8 Stable tag: 1. 2.28 Stable tag: 1.3.0 9 9 License: GPLv2 or later 10 10 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 15 15 == Description == 16 16 Media Tracker is a powerful WordPress plugin designed to help you identify and remove unused media files, manage duplicate images, and streamline your media library for better site performance and storage efficiency. Boost your WordPress site’s speed and organization with Media Tracker, the ultimate solution for managing and optimizing media files. Effortlessly track, organize, and clean up unused images to maintain an efficient and clutter-free media library. With Media Tracker, you can easily locate where each image is used across posts, pages, and custom post types, enhancing your website's performance and user experience. 17 18 [youtube https://www.youtube.com/watch?v=2eMRuW5X-iI] 17 19 18 20 ## Features … … 57 59 58 60 == Screenshots == 59 1. Example of media tracking report. 60 2. Unused media cleaner interface. 61 3. Find dupliacte image 61 1. Media Tracker Dashboard 62 2. Example of media tracking report. 63 3. Unused media cleaner interface. 64 4. Find dupliacte image 65 5. Documentations 62 66 63 67 == Changelog == 68 = 1.3.0 [05/02/2026] = 69 * New: Complete design overhaul with a modern, unified Dashboard interface 70 * New: Consolidated all tools (Unused Media, Duplicates) into a single "Media Tracker" page with tabbed navigation 71 * New: "Overview" tab providing a high-level summary of library usage and stats 72 * New: Dedicated sections for Pro features (Optimization, Security, External Storage, Multi-site) 73 * New: "Remove All" button added to Unused Media scanner for bulk cleanup 74 * New: "Documents" tab for managing document with video tutorials 75 * Enhanced: Rebuilt stylesheets using SCSS for better maintainability and consistency 76 * Enhanced: Redesigned progress bars for Unused Media and Duplicate scans 77 * Enhanced: Improved Media Usage table layout and responsiveness 78 * Fixed: Critical issue with Scan buttons not responding in some scenarios 79 * Fixed: Tab navigation state lost on page reload (URL handling fixed) 80 * Fixed: "Direct DB Query" warnings by optimizing database calls 81 * Fixed: PHPCS compliance issues (variable prefixing, escaping) 82 * Fixed: Cron schedule registration to prevent "invalid_schedule" errors 83 * Fixed: Overview tab unused media count accuracy 84 * Internal: Codebase improvements and optimization 85 64 86 = 1.2.2 [17/01/2026] = 65 87 * Fixed: Deactivation feedback form now correctly sends email and deactivates plugin -
media-tracker/trunk/vendor/composer/InstalledVersions.php
r3140549 r3454648 28 28 { 29 29 /** 30 * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to 31 * @internal 32 */ 33 private static $selfDir = null; 34 35 /** 30 36 * @var mixed[]|null 31 37 * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null 32 38 */ 33 39 private static $installed; 40 41 /** 42 * @var bool 43 */ 44 private static $installedIsLocalDir; 34 45 35 46 /** … … 310 321 self::$installed = $data; 311 322 self::$installedByVendor = array(); 323 324 // when using reload, we disable the duplicate protection to ensure that self::$installed data is 325 // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, 326 // so we have to assume it does not, and that may result in duplicate data being returned when listing 327 // all installed packages for example 328 self::$installedIsLocalDir = false; 329 } 330 331 /** 332 * @return string 333 */ 334 private static function getSelfDir() 335 { 336 if (self::$selfDir === null) { 337 self::$selfDir = strtr(__DIR__, '\\', '/'); 338 } 339 340 return self::$selfDir; 312 341 } 313 342 … … 323 352 324 353 $installed = array(); 354 $copiedLocalDir = false; 325 355 326 356 if (self::$canGetVendors) { 357 $selfDir = self::getSelfDir(); 327 358 foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 359 $vendorDir = strtr($vendorDir, '\\', '/'); 328 360 if (isset(self::$installedByVendor[$vendorDir])) { 329 361 $installed[] = self::$installedByVendor[$vendorDir]; … … 331 363 /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ 332 364 $required = require $vendorDir.'/composer/installed.php'; 333 $installed[] = self::$installedByVendor[$vendorDir] = $required; 334 if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 335 self::$installed = $installed[count($installed) - 1]; 365 self::$installedByVendor[$vendorDir] = $required; 366 $installed[] = $required; 367 if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { 368 self::$installed = $required; 369 self::$installedIsLocalDir = true; 336 370 } 371 } 372 if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { 373 $copiedLocalDir = true; 337 374 } 338 375 } … … 351 388 } 352 389 353 if (self::$installed !== array() ) {390 if (self::$installed !== array() && !$copiedLocalDir) { 354 391 $installed[] = self::$installed; 355 392 } -
media-tracker/trunk/vendor/composer/autoload_real.php
r3151282 r3454648 32 32 $loader->register(true); 33 33 34 $filesToLoad = \Composer\Autoload\ComposerStaticInit27ae33a58e56550f0fd0ae9ff16605b6::$files; 35 $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { 36 if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { 37 $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; 38 39 require $file; 40 } 41 }, null, null); 42 foreach ($filesToLoad as $fileIdentifier => $file) { 43 $requireFile($fileIdentifier, $file); 44 } 45 34 46 return $loader; 35 47 } -
media-tracker/trunk/vendor/composer/autoload_static.php
r3151282 r3454648 7 7 class ComposerStaticInit27ae33a58e56550f0fd0ae9ff16605b6 8 8 { 9 public static $files = array ( 10 '5ca4469d9069a9cdada698d89b95c729' => __DIR__ . '/../..' . '/includes/functions.php', 11 ); 12 9 13 public static $prefixLengthsPsr4 = array ( 10 14 'M' => -
media-tracker/trunk/vendor/composer/installed.php
r3151282 r3454648 4 4 'pretty_version' => 'dev-main', 5 5 'version' => 'dev-main', 6 'reference' => ' 8c342a3e99839e4b78d4e710e735baa1877a6580',6 'reference' => '274607d53e2c30110379855a4fcfc42dd0045d97', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 14 14 'pretty_version' => 'dev-main', 15 15 'version' => 'dev-main', 16 'reference' => ' 8c342a3e99839e4b78d4e710e735baa1877a6580',16 'reference' => '274607d53e2c30110379855a4fcfc42dd0045d97', 17 17 'type' => 'wordpress-plugin', 18 18 'install_path' => __DIR__ . '/../../',
Note: See TracChangeset
for help on using the changeset viewer.