Changeset 3457950
- Timestamp:
- 02/10/2026 11:41:43 AM (8 days ago)
- Location:
- media-tracker
- Files:
-
- 79 added
- 22 edited
-
tags/1.3.2 (added)
-
tags/1.3.2/assets (added)
-
tags/1.3.2/assets/dist (added)
-
tags/1.3.2/assets/dist/css (added)
-
tags/1.3.2/assets/dist/css/mt-admin.css (added)
-
tags/1.3.2/assets/dist/css/pro-lock.css (added)
-
tags/1.3.2/assets/dist/images (added)
-
tags/1.3.2/assets/dist/images/logo.svg (added)
-
tags/1.3.2/assets/dist/images/mt-pro-1.webp (added)
-
tags/1.3.2/assets/dist/images/mt-pro-2.webp (added)
-
tags/1.3.2/assets/dist/images/mt-pro-3.webp (added)
-
tags/1.3.2/assets/dist/images/mt-pro-5.webp (added)
-
tags/1.3.2/assets/dist/images/youtube-thumb.webp (added)
-
tags/1.3.2/assets/dist/js (added)
-
tags/1.3.2/assets/dist/js/mt-admin.js (added)
-
tags/1.3.2/assets/dist/js/tab.js (added)
-
tags/1.3.2/assets/src (added)
-
tags/1.3.2/assets/src/images (added)
-
tags/1.3.2/assets/src/images/logo.svg (added)
-
tags/1.3.2/assets/src/images/mt-pro-1.webp (added)
-
tags/1.3.2/assets/src/images/mt-pro-2.webp (added)
-
tags/1.3.2/assets/src/images/mt-pro-3.webp (added)
-
tags/1.3.2/assets/src/images/mt-pro-5.webp (added)
-
tags/1.3.2/assets/src/images/youtube-thumb.webp (added)
-
tags/1.3.2/assets/src/js (added)
-
tags/1.3.2/assets/src/js/mt-admin.js (added)
-
tags/1.3.2/assets/src/js/tab.js (added)
-
tags/1.3.2/assets/src/scss (added)
-
tags/1.3.2/assets/src/scss/mt-admin.scss (added)
-
tags/1.3.2/assets/src/scss/pro-lock.scss (added)
-
tags/1.3.2/composer.json (added)
-
tags/1.3.2/gulp.config.js (added)
-
tags/1.3.2/gulpfile.babel.js (added)
-
tags/1.3.2/includes (added)
-
tags/1.3.2/includes/Admin (added)
-
tags/1.3.2/includes/Admin.php (added)
-
tags/1.3.2/includes/Admin/Duplicate_Images.php (added)
-
tags/1.3.2/includes/Admin/Media_Usage.php (added)
-
tags/1.3.2/includes/Admin/Menu.php (added)
-
tags/1.3.2/includes/Admin/PluginMeta.php (added)
-
tags/1.3.2/includes/Admin/Unused_Media_List.php (added)
-
tags/1.3.2/includes/Admin/views (added)
-
tags/1.3.2/includes/Admin/views/media-tracker.php (added)
-
tags/1.3.2/includes/Admin/views/tabs (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-documents.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-duplicates.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-external-storage.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-license.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-multisite.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-optimization.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-overview.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-security.php (added)
-
tags/1.3.2/includes/Admin/views/tabs/tab-unused-media.php (added)
-
tags/1.3.2/includes/Admin/views/unused-media-list.php (added)
-
tags/1.3.2/includes/Assets.php (added)
-
tags/1.3.2/includes/Cron_Schedules.php (added)
-
tags/1.3.2/includes/Installer.php (added)
-
tags/1.3.2/includes/Media_Tracker_i18n.php (added)
-
tags/1.3.2/includes/functions.php (added)
-
tags/1.3.2/languages (added)
-
tags/1.3.2/languages/media-tracker.pot (added)
-
tags/1.3.2/media-tracker.php (added)
-
tags/1.3.2/package.json (added)
-
tags/1.3.2/readme.txt (added)
-
tags/1.3.2/vendor (added)
-
tags/1.3.2/vendor/autoload.php (added)
-
tags/1.3.2/vendor/composer (added)
-
tags/1.3.2/vendor/composer/ClassLoader.php (added)
-
tags/1.3.2/vendor/composer/InstalledVersions.php (added)
-
tags/1.3.2/vendor/composer/LICENSE (added)
-
tags/1.3.2/vendor/composer/autoload_classmap.php (added)
-
tags/1.3.2/vendor/composer/autoload_files.php (added)
-
tags/1.3.2/vendor/composer/autoload_namespaces.php (added)
-
tags/1.3.2/vendor/composer/autoload_psr4.php (added)
-
tags/1.3.2/vendor/composer/autoload_real.php (added)
-
tags/1.3.2/vendor/composer/autoload_static.php (added)
-
tags/1.3.2/vendor/composer/installed.json (added)
-
tags/1.3.2/vendor/composer/installed.php (added)
-
trunk/assets/dist/css/mt-admin.css (modified) (1 diff)
-
trunk/assets/dist/css/pro-lock.css (modified) (1 diff)
-
trunk/assets/dist/js/mt-admin.js (modified) (1 diff)
-
trunk/assets/dist/js/tab.js (modified) (1 diff)
-
trunk/assets/src/js/mt-admin.js (modified) (1 diff)
-
trunk/assets/src/js/tab.js (modified) (4 diffs)
-
trunk/assets/src/scss/mt-admin.scss (modified) (10 diffs)
-
trunk/includes/Admin.php (modified) (1 diff)
-
trunk/includes/Admin/Duplicate_Images.php (modified) (4 diffs)
-
trunk/includes/Admin/Media_Usage.php (modified) (6 diffs)
-
trunk/includes/Admin/Menu.php (modified) (9 diffs)
-
trunk/includes/Admin/Unused_Media_List.php (modified) (47 diffs)
-
trunk/includes/Admin/views/media-tracker.php (modified) (1 diff)
-
trunk/includes/Admin/views/tabs/tab-duplicates.php (modified) (3 diffs)
-
trunk/includes/Admin/views/tabs/tab-license.php (added)
-
trunk/includes/Admin/views/tabs/tab-overview.php (modified) (11 diffs)
-
trunk/includes/Admin/views/unused-media-list.php (modified) (9 diffs)
-
trunk/includes/Assets.php (modified) (1 diff)
-
trunk/includes/Installer.php (modified) (9 diffs)
-
trunk/includes/functions.php (modified) (13 diffs)
-
trunk/languages/media-tracker.pot (modified) (13 diffs)
-
trunk/media-tracker.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
media-tracker/trunk/assets/dist/css/mt-admin.css
r3455634 r3457950 1 #mt-feedback-modal{ position:fixed;z-index:10000;left:0;top:0;width:100%;height:100%;background-color:rgba(0, 0, 0, .3);display:none;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}#mt-feedback-modal .mt-feedback-modal-content{background:#fff;padding:30px;border-radius:8px;width:80%;max-width:600px;-webkit-box-shadow:0 4px 8px rgba(0, 0, 0, .2);box-shadow:0 4px 8px rgba(0, 0, 0, .2);position:relative;text-align:center;margin:10% auto}#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{margin-top:0;font-size:1.5em;color:#333;line-height:normal}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header .close{position:absolute;top:10px;right:10px;font-size:1.5em;cursor:pointer}#mt-feedback-modal .mt-feedback-modal-content .mt-feedback-modal-body textarea{width:100%;height:120px;margin:20px 0;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:1em;resize:vertical}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button{background-color:#007cba;color:#fff;border:none;border-radius:4px;padding:10px 20px;font-size:1em;cursor:pointer;margin:5px;-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:rgba(0, 0, 0, 0);color:#2271b1;border:1px solid #2271b1}.broken-link-checker .post_title{padding-left:15px !important}.broken-link-checker #post_type{padding:0}.wrap.unused-media-list .wp-heading-inline,.wrap.broken-link-checker .wp-heading-inline{width:calc(100% - 40px);background:#28a745;color:#fff;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:20px 20px;gap:20px;border-radius:5px;margin-top:15px;margin-bottom:10px}.wrap.unused-media-list .wp-heading-inline svg,.wrap.broken-link-checker .wp-heading-inline svg{width:40px;height:40px}.media-tracker-layout{margin:0;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-sizing:inherit;box-sizing:inherit;background-color:#f8fafc;color:#1e293b;display:-webkit-box;display:-ms-flexbox;display:flex;min-height:100vh;margin-top:24px;width:98.8%}.media-tracker-layout h2{margin:0;padding:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px}.media-tracker-layout aside{width:260px;background:#0f172a;color:#fff;padding:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding:12px}.media-tracker-layout aside .version{text-align:center;padding:15px}.media-tracker-layout .logo{font-size:19.2px;font-size:1.2rem;font-weight:bold;margin:10px 0px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;color:#fff}.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{padding:13px 10px;cursor:pointer;-webkit-transition:.25s;transition:.25s;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;gap:10px;font-size:14px;color:#fff;letter-spacing:.1px;margin:0 0 3px;border-radius:4px}.media-tracker-layout nav li:hover,.media-tracker-layout nav li.active{background:#6366f1;color:#fff}.media-tracker-layout nav li i{width:20px;text-align:left}.media-tracker-layout ul li a{color:#fff;text-decoration:none;width:100%;padding:15px;outline:none;border:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;text-decoration:none;gap:10px}.media-tracker-layout ul li a small{margin-left:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.media-tracker-layout ul li a:focus{outline:none;-webkit-box-shadow:none;box-shadow:none}.media-tracker-layout ul li.license,.media-tracker-layout ul li.settings,.media-tracker-layout ul li.multisite{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;-ms-flex:1;flex:1;padding:32px;padding:2rem;overflow-y:auto;background:#fff}.media-tracker-layout header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:32px;margin-bottom:2rem}.media-tracker-layout .status-badge{background:#dcfce7;color:#166534;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600}.media-tracker-layout h1{font-size:22px;font-weight:600;letter-spacing:-0.01em;margin:0}.media-tracker-layout .stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(240px, 1fr));gap:24px;gap:1.5rem;margin-bottom:24px;margin-bottom:1.5rem}.media-tracker-layout .card{max-width:100%;background:#fff;padding:24px;padding:1.5rem;border-radius:10px;border:1px solid #e2e8f0;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, .05);box-shadow:0 1px 3px rgba(0, 0, 0, .05);margin:0}.media-tracker-layout .card table .btn{font-size:12px;width:80px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.media-tracker-layout .card h3{font-size:16px;color:#313335;margin-bottom:12px;margin-top:0}.media-tracker-layout .card h3 i{color:#6366f1}.media-tracker-layout .card .value{font-size:18px;line-height:normal;font-weight:bold;display:block;margin-bottom:3px}.media-tracker-layout .progress-bar{height:8px;background:#e5e7eb;border-radius:4px;margin-top:10px;overflow:hidden}.media-tracker-layout .progress-fill{height:100%;background:#6366f1;border-radius:4px}.media-tracker-layout .section-title{font-size:16px}.media-tracker-layout .grid-two{display:grid;grid-template-columns:2.05fr 1fr;gap:24px;gap:1.5rem}.media-tracker-layout .grid-three{display:grid;grid-template-columns:1fr 1fr 1fr;gap:24px;gap:1.5rem}.media-tracker-layout .setting-item{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:12px 0;border-bottom:1px solid #e2e8f0}.media-tracker-layout .setting-item p{margin:0}.media-tracker-layout .setting-item:last-child{border:none}.media-tracker-layout .switch{position:relative;display:inline-block;width:44px;height:22px}.media-tracker-layout .switch input{opacity:0;width:0;height:0}.media-tracker-layout .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#cbd5f5;-webkit-transition:.25s;transition:.25s;border-radius:34px}.media-tracker-layout .slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;bottom:3px;background-color:#fff;-webkit-transition:.25s;transition:.25s;border-radius:50%}.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{padding:12px 20px;border-radius:6px;border:none;font-weight:600;cursor:pointer;-webkit-transition:.2s;transition:.2s;font-size:14px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;gap:5px;text-decoration:none}.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:rgba(0, 0, 0, 0);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{width:100%;border-collapse:collapse;margin-top:16px;margin-top:1rem}.media-tracker-layout th{text-align:left;padding:12px;font-size:14px;color:#64748b}.media-tracker-layout th:last-child{text-align:center}.media-tracker-layout td{padding:12px;border-bottom:1px solid #c3c4c7;font-size:14px}.media-tracker-layout tr:last-child td{border-bottom:none}.media-tracker-layout .tag{padding:2px 8px;border-radius:4px;font-size:11px;font-weight:bold;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{font-size:13px;color:#64748b;margin-top:10px;margin-bottom:0}.media-tracker-layout .stacked{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:10px;margin-top:10px}.media-tracker-layout .inline-actions{display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.media-tracker-layout .modal-backdrop{position:fixed;inset:0;background:rgba(15, 23, 42, .55);display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;z-index:50}.media-tracker-layout .modal-backdrop.active{display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .modal{background:#fff;border-radius:12px;padding:24px;padding:1.5rem;width:480px;max-width:94%;-webkit-box-shadow:0 20px 40px rgba(15, 23, 42, .3);box-shadow:0 20px 40px rgba(15, 23, 42, .3);border:1px solid #e2e8f0}.media-tracker-layout .modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:16px;margin-bottom:1rem}.media-tracker-layout .modal-title{font-size:18px;font-weight:600;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px}.media-tracker-layout .modal-close{border:none;background:rgba(0, 0, 0, 0);cursor:pointer;font-size:18px;color:#64748b}.media-tracker-layout .modal-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:12px;margin-bottom:24px;margin-bottom:1.5rem}.media-tracker-layout .modal-body label{font-size:13px;margin-bottom:4px;display:block}.media-tracker-layout .modal-body input,.media-tracker-layout .modal-body select{width:100%;padding:8px 10px;border-radius:6px;border:1px solid #e2e8f0;font-size:13px}.media-tracker-layout .modal-body input:focus,.media-tracker-layout .modal-body select:focus{outline:none;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)}.media-tracker-layout .modal-hint{font-size:11px;color:#64748b}.media-tracker-layout .modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;gap:10px}.media-tracker-layout .mediatracker-usage-table{margin-top:15px}.media-tracker-layout .mediatracker-usage-table table.wp-list-table th,.media-tracker-layout .mediatracker-usage-table table.wp-list-table td{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;margin-bottom:.2em;font-size:14px}.media-tracker-layout .unused-media-list .wp-list-table .media-icon{float:left;min-height:60px;margin:0 9px 0 0}.media-tracker-layout .unused-media-list .wp-list-table .media-icon img{width:60px;height:60px}.media-tracker-layout .unused-media-list .wp-list-table.fixed{table-layout:inherit}.media-tracker-layout .unused-media-list .search-box{margin:0px}.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{table-layout:inherit;width:100%;margin-top:20px}.media-tracker-layout .unused-media-list table.widefat th,.media-tracker-layout .unused-media-list table.widefat td{padding:10px;text-align:left}.media-tracker-layout .unused-media-list table.widefat th.status,.media-tracker-layout .unused-media-list table.widefat td.status{font-weight:700;color:red}.media-tracker-layout .media-toolbar-wrap.wp-filter{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:20px;border:none;-webkit-box-shadow:none;box-shadow:none}.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{font-size:20px;font-weight:bold;color:#6366f1}.media-tracker-layout .replace-broken-link input{width:100%}.media-tracker-layout #clear-broken-links-transient{position:absolute;padding:8px 30px;font-size:14px;font-weight:500}.media-tracker-layout #success-message{display:none;color:green;margin-top:15px;position:absolute;left:230px;font-size:16px}.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:rgba(0, 0, 0, 0);border-bottom:1px solid #c3c4c7;width:3.2em;padding:16px 3px}.media-tracker-layout #mt-duplicate-form .duplicate-media-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:30px;-webkit-box-pack:justify;-ms-flex-pack:justify;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{font-size:20px;color:#6366f1;font-weight:700}.media-tracker-layout .duplicate-media-count h2 span{color:#1d2327;font-weight:400}.media-tracker-layout .media-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:30px}.media-tracker-layout #tab-unused-media .tablenav.bottom{margin-top:15px}.media-tracker-layout .mt-overview-table{border-radius:8px;overflow:hidden;-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;width:100%;border-collapse:separate;border-spacing:0;background:#fff}.media-tracker-layout .mt-overview-table thead th{background-color:#f8fafc;color:#475569;font-weight:600;text-transform:uppercase;font-size:11px;letter-spacing:.05em;padding:16px;border-bottom:1px solid #e2e8f0;text-align:left}.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(even){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{padding:16px;color:#334155;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:14px}.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{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;gap:8px}.media-tracker-layout .mt-flex-center{display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.media-tracker-layout .mt-stat-title{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;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{font-size:12px;color:#64748b}.media-tracker-layout .mt-btn-sm{padding:6px 12px !important;font-size:12px !important}.media-tracker-layout .mt-btn-xs{padding:5px 10px !important}.media-tracker-layout .mt-mime-item{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;padding:10px;background:#f8fafc;border-radius:8px}.media-tracker-layout .mt-mime-icon{color:#6366f1;width:20px;text-align:center}.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;text-align:center;padding:20px}.media-tracker-layout .mt-mt-6{margin-top:24px;margin-top:1.5rem}.media-tracker-layout .mt-empty-state-large{color:#64748b;text-align:center;padding:40px}.media-tracker-layout .mt-success-icon-large{font-size:48px;color:#10b981;margin-bottom:15px;height:48px;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{font-size:48px;color:#cbd5e1;margin-bottom:15px;height:48px;width:48px}.media-tracker-layout .mt-mb-3{margin-bottom:12px}.media-tracker-layout .mt-progress-text{margin-left:8px;font-size:11px;color:#64748b;min-width:40px;text-align:right;display:none}.media-tracker-layout .mt-status-text{margin-left:4px;display:none}.media-tracker-layout .mt-progress-track{-webkit-box-flex:1;-ms-flex:1;flex:1;max-width:100%;height:15px;background:-webkit-gradient(linear, left top, right top, from(#eef2ff), to(#e2e8f0));background:linear-gradient(90deg, #eef2ff, #e2e8f0);border-radius:999px;overflow:hidden;display:none;-webkit-box-shadow:0 0 0 1px rgba(148, 163, 184, .4);box-shadow:0 0 0 1px rgba(148, 163, 184, .4)}.media-tracker-layout .mt-progress-bar-fill{width:0%;height:100%;background:#6366f1}.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{width:60px;height:auto}.media-tracker-layout .mt-link-clean{text-decoration:none;font-weight:500}@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{width:100%;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1.5rem}.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{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-webkit-transition:all .3s ease;transition:all .3s ease;cursor:pointer;-webkit-box-shadow:0 4px 6px -1px rgba(0, 0, 0, .05);box-shadow:0 4px 6px -1px rgba(0, 0, 0, .05);width:32%}.media-video-featured-card:hover{-webkit-transform:translateY(-2px);transform:translateY(-2px);-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);border-color:#cbd5e1}.media-video-featured-card:hover .video-thumbnail{opacity:.6}.media-video-featured-card:hover .play-icon{-webkit-transform:scale(1.1);transform:scale(1.1);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)}.media-video-featured-card .video-thumbnail-wrapper{position:relative;background:#000;overflow:hidden;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.media-video-featured-card .video-thumbnail{width:100%;height:100%;-o-object-fit:cover;object-fit:cover;opacity:.8;-webkit-transition:opacity .3s ease;transition:opacity .3s ease}.media-video-featured-card .play-button-overlay{position:absolute;top:0;left:0;width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;z-index:2}.media-video-featured-card .play-icon{width:60px;height:60px;background:hsla(0, 0%, 100%, .9);border-radius:50%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-transition:all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);transition:all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);-webkit-box-shadow:0 0 0 0 hsla(0, 0%, 100%, .7);box-shadow:0 0 0 0 hsla(0, 0%, 100%, .7)}.media-video-featured-card .play-icon .dashicons{font-size:32px;width:32px;height:32px;color:#6366f1;margin-left:4px}.media-video-featured-card .video-card-content{padding:32px;padding:2rem;-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-video-featured-card .video-card-content h4{margin:0 0 12px 0;margin:0 0 .75rem 0;font-size:20px;font-size:1.25rem;color:#1e293b;font-weight:600}.mt-video-modal-backdrop{position:fixed;inset:0;background:rgba(15, 23, 42, .75);z-index:100000;display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;backdrop-filter:blur(4px);opacity:0;-webkit-transition:opacity .3s ease;transition:opacity .3s ease}.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;width:90%;max-width:800px;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);overflow:hidden;-webkit-transform:scale(0.95);transform:scale(0.95);-webkit-transition:-webkit-transform .3s cubic-bezier(0.34, 1.56, 0.64, 1);transition:-webkit-transform .3s cubic-bezier(0.34, 1.56, 0.64, 1);transition:transform .3s cubic-bezier(0.34, 1.56, 0.64, 1);transition:transform .3s cubic-bezier(0.34, 1.56, 0.64, 1), -webkit-transform .3s cubic-bezier(0.34, 1.56, 0.64, 1)}.mt-video-modal-header{padding:16px 24px;padding:1rem 1.5rem;border-bottom:1px solid #e2e8f0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f8fafc}.mt-video-modal-header h3{margin:0;font-size:17.6px;font-size:1.1rem;color:#334155}.mt-video-modal-close{background:rgba(0, 0, 0, 0);border:none;cursor:pointer;color:#64748b;padding:4px;border-radius:4px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-transition:background .2s;transition:background .2s}.mt-video-modal-close:hover{background:#e2e8f0;color:#ef4444}.mt-video-modal-body{padding:0;background:#000}.mt-responsive-video-wrapper{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.mt-responsive-video-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0}@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{width:100%;height:200px}.media-video-featured-card .video-card-content{padding:1.5rem}}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}#usage_count{width:140px!important}.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;-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: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 table td{vertical-align:middle}.media-tracker-layout #mt-duplicate-form table .mt-dup-delete-single{border:1px solid #6366f1;color:#6366f1;height:32px;line-height:normal;padding:8px 4px;-webkit-transition:.3s;transition:.3s;vertical-align:middle;width:32px}.media-tracker-layout #mt-duplicate-form table .mt-dup-delete-single span{font-size:14px}.media-tracker-layout #mt-duplicate-form table .mt-dup-delete-single:hover{background:#6366f1;color:#fff}.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 .duplicate-media-footer .tablenav{margin:0;padding:0}.media-tracker-layout #mt-duplicate-form .tablenav-pages .paging-input{margin:0 15px}.media-tracker-layout #tab-duplicates #mt-dup-scan{background-color:#6366f1;border:none;color:#fff;padding:4px 14px}.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:1px solid #e2e8f0;border-collapse:separate;border-radius:4px;border-spacing:0;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:4px}.media-tracker-layout .mt-overview-table thead tr:first-child th:last-child{border-top-right-radius:4px}.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{border-radius:4px;height:52px;-o-object-fit:cover;object-fit:cover;width:52px}.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/pro-lock.css
r3455634 r3457950 1 .mt-pro-lock-wrapper{ position:relative;min-height:400px}.mt-pro-lock-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:hsla(0, 0%, 100%, .7);z-index:100;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-top:100px;backdrop-filter:blur(2px)}.mt-pro-lock-modal{background:#fff;border-radius:8px;-webkit-box-shadow:0 10px 40px rgba(0, 0, 0, .15),0 2px 10px rgba(0, 0, 0, .08);box-shadow:0 10px 40px rgba(0, 0, 0, .15),0 2px 10px rgba(0, 0, 0, .08);max-width:600px;width:90%;overflow:hidden;-webkit-animation:mtModalFadeIn .3s ease-out;animation:mtModalFadeIn .3s ease-out}@-webkit-keyframes mtModalFadeIn{from{opacity:0;-webkit-transform:translateY(-10px);transform:translateY(-10px)}to{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes mtModalFadeIn{from{opacity:0;-webkit-transform:translateY(-10px);transform:translateY(-10px)}to{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.mt-pro-lock-header{background:#f3f0ff;border-bottom:1px solid #ddd6fe;padding:12px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px}.mt-pro-lock-icon{color:#6366f1;font-size:16px}.mt-pro-lock-notice{color:#5b21b6;font-size:13px;font-weight:500}.mt-pro-lock-body{padding:28px 32px;text-align:center}.mt-pro-lock-body h2{margin:0 0 12px;font-size:22px;font-weight:600;color:#1e293b}.mt-pro-lock-body>p{margin:0 0 24px;color:#64748b;font-size:14px;line-height:1.6}.mt-pro-lock-features{list-style:none;margin:0 0 28px;padding:0;display:grid;grid-template-columns:repeat(2, 1fr);gap:12px;text-align:left}.mt-pro-lock-features li{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:8px;font-size:13px;color:#334155}.mt-pro-lock-body h2{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.mt-pro-lock-features li i{color:#6366f1;font-size:22 px;-ms-flex-negative:0;flex-shrink:0}.mt-pro-lock-button{display:inline-block;background:linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);color:#fff !important;text-decoration:none !important;padding:14px 32px;border-radius:6px;font-size:15px;font-weight:600;-webkit-transition:all .2s ease;transition:all .2s ease;-webkit-box-shadow:0 4px 12px rgba(95, 28, 252, .35);box-shadow:0 4px 12px rgba(95, 28, 252, .35)}.mt-pro-lock-button:hover{background:linear-gradient(135deg, #6366f1 0%, #4c1d95 100%);-webkit-transform:translateY(-1px);transform:translateY(-1px)}.mt-pro-lock-button:active{-webkit-transform:translateY(0);transform:translateY(0)}.mt-pro-lock-content{filter:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><filter id="filter"><feGaussianBlur stdDeviation="3" /></filter></svg>#filter');-webkit-filter:blur(3px);filter:blur(3px);pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:.6}@media(max-width: 600px){.mt-pro-lock-features{grid-template-columns:1fr}.mt-pro-lock-modal{width:95%}.mt-pro-lock-body{padding:20px}.mt-pro-lock-body h2{font-size:18px}}.media-info-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));gap:24px;gap:1.5rem;margin-top:24px;margin-top:1.5rem}.info-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:24px;-webkit-box-shadow:0 4px 6px -1px rgba(0, 0, 0, .05);box-shadow:0 4px 6px -1px rgba(0, 0, 0, .05);-webkit-transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:transform .2s,box-shadow .2s;transition:transform .2s,box-shadow .2s,-webkit-transform .2s,-webkit-box-shadow .2s;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.info-card:hover{-webkit-transform:translateY(-2px);transform:translateY(-2px);-webkit-box-shadow:0 10px 15px -3px rgba(0, 0, 0, .1);box-shadow:0 10px 15px -3px rgba(0, 0, 0, .1)}.info-card-icon{width:48px;height:48px;border-radius:10px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;font-size:24px;margin-bottom:16px}.info-card-title{font-size:18px;font-weight:600;color:#1e293b;margin:0 0 8px 0}.info-card-desc{font-size:14px;color:#64748b;line-height:1.6;margin:0 0 20px 0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.info-card-link{color:#1e293b;font-weight:500;text-decoration:none;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:14px;text-decoration:none !important}.info-card-link:hover{color:#4f46e5}.info-card-link .dashicons{font-size:16px;width:16px;height:16px;margin-left:4px;margin-top:2px}@media(max-width: 768px){.media-info-grid{grid-template-columns:1fr}.media-video-grid{grid-template-columns:1fr}}.media-video-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));gap:24px;gap:1.5rem}.video-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;-webkit-box-shadow:0 4px 6px -1px rgba(0, 0, 0, .05);box-shadow:0 4px 6px -1px rgba(0, 0, 0, .05);-webkit-transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:transform .2s,box-shadow .2s;transition:transform .2s,box-shadow .2s,-webkit-transform .2s,-webkit-box-shadow .2s}.video-card:hover{-webkit-transform:translateY(-2px);transform:translateY(-2px);-webkit-box-shadow:0 10px 15px -3px rgba(0, 0, 0, .1);box-shadow:0 10px 15px -3px rgba(0, 0, 0, .1)}.video-wrapper{position:relative;padding-bottom:56.25%;height:0;background:#000}.video-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%}.video-card-content{padding:16px;padding:1rem}.video-card-content h4{margin:0 0 8px 0;margin:0 0 .5rem 0;font-size:16px;color:#1e293b}.video-card-content p{margin:0;font-size:14px;color:#64748b}1 .mt-pro-lock-wrapper{min-height:400px;position:relative}.mt-pro-lock-overlay{-webkit-box-align:start;-ms-flex-align:start;-webkit-box-pack:center;-ms-flex-pack:center;align-items:flex-start;backdrop-filter:blur(2px);background:hsla(0,0%,100%,.7);bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center;left:0;padding-top:100px;position:absolute;right:0;top:0;z-index:100}.mt-pro-lock-modal{-webkit-animation:mtModalFadeIn .3s ease-out;animation:mtModalFadeIn .3s ease-out;background:#fff;border-radius:8px;-webkit-box-shadow:0 10px 40px rgba(0,0,0,.15),0 2px 10px rgba(0,0,0,.08);box-shadow:0 10px 40px rgba(0,0,0,.15),0 2px 10px rgba(0,0,0,.08);max-width:600px;overflow:hidden;width:90%}@-webkit-keyframes mtModalFadeIn{0%{opacity:0;-webkit-transform:translateY(-10px);transform:translateY(-10px)}to{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes mtModalFadeIn{0%{opacity:0;-webkit-transform:translateY(-10px);transform:translateY(-10px)}to{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.mt-pro-lock-header{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f3f0ff;border-bottom:1px solid #ddd6fe;display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;padding:12px 20px}.mt-pro-lock-icon{color:#6366f1;font-size:16px}.mt-pro-lock-notice{color:#5b21b6;font-size:13px;font-weight:500}.mt-pro-lock-body{padding:28px 32px;text-align:center}.mt-pro-lock-body h2{color:#1e293b;font-size:22px;font-weight:600;margin:0 0 12px}.mt-pro-lock-body>p{color:#64748b;font-size:14px;line-height:1.6;margin:0 0 24px}.mt-pro-lock-features{display:grid;gap:12px;grid-template-columns:repeat(2,1fr);list-style:none;margin:0 0 28px;padding:0;text-align:left}.mt-pro-lock-features li{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#334155;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:13px;gap:8px}.mt-pro-lock-body h2{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.mt-pro-lock-features li i{-ms-flex-negative:0;color:#6366f1;flex-shrink:0;font-size:22 px}.mt-pro-lock-button{background:linear-gradient(135deg,#7c3aed,#6366f1);border-radius:6px;-webkit-box-shadow:0 4px 12px rgba(95,28,252,.35);box-shadow:0 4px 12px rgba(95,28,252,.35);color:#fff!important;display:inline-block;font-size:15px;font-weight:600;padding:14px 32px;text-decoration:none!important;-webkit-transition:all .2s ease;transition:all .2s ease}.mt-pro-lock-button:hover{background:linear-gradient(135deg,#6366f1,#4c1d95);-webkit-transform:translateY(-1px);transform:translateY(-1px)}.mt-pro-lock-button:active{-webkit-transform:translateY(0);transform:translateY(0)}.mt-pro-lock-content{filter:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><filter id="filter"><feGaussianBlur stdDeviation="3" /></filter></svg>#filter');-webkit-filter:blur(3px);filter:blur(3px);opacity:.6;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media(max-width:600px){.mt-pro-lock-features{grid-template-columns:1fr}.mt-pro-lock-modal{width:95%}.mt-pro-lock-body{padding:20px}.mt-pro-lock-body h2{font-size:18px}}.media-info-grid{display:grid;gap:24px;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));margin-top:1.5rem}.info-card{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;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);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:24px;-webkit-transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:transform .2s,box-shadow .2s;transition:transform .2s,box-shadow .2s,-webkit-transform .2s,-webkit-box-shadow .2s}.info-card:hover{-webkit-box-shadow:0 10px 15px -3px rgba(0,0,0,.1);box-shadow:0 10px 15px -3px rgba(0,0,0,.1);-webkit-transform:translateY(-2px);transform:translateY(-2px)}.info-card-icon{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;border-radius:10px;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:24px;height:48px;justify-content:center;margin-bottom:16px;width:48px}.info-card-title{color:#1e293b;font-size:18px;font-weight:600;margin:0 0 8px}.info-card-desc{-webkit-box-flex:1;-ms-flex-positive:1;color:#64748b;flex-grow:1;font-size:14px;line-height:1.6;margin:0 0 20px}.info-card-link{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#1e293b;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;font-size:14px;font-weight:500;text-decoration:none;text-decoration:none!important}.info-card-link:hover{color:#4f46e5}.info-card-link .dashicons{font-size:16px;height:16px;margin-left:4px;margin-top:2px;width:16px}@media(max-width:768px){.media-info-grid,.media-video-grid{grid-template-columns:1fr}}.media-video-grid{display:grid;gap:24px;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}.video-card{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);overflow:hidden;-webkit-transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:-webkit-transform .2s,-webkit-box-shadow .2s;transition:transform .2s,box-shadow .2s;transition:transform .2s,box-shadow .2s,-webkit-transform .2s,-webkit-box-shadow .2s}.video-card:hover{-webkit-box-shadow:0 10px 15px -3px rgba(0,0,0,.1);box-shadow:0 10px 15px -3px rgba(0,0,0,.1);-webkit-transform:translateY(-2px);transform:translateY(-2px)}.video-wrapper{background:#000;height:0;padding-bottom:56.25%;position:relative}.video-wrapper iframe{height:100%;left:0;position:absolute;top:0;width:100%}.video-card-content{padding:1rem}.video-card-content h4{color:#1e293b;font-size:16px;margin:0 0 .5rem}.video-card-content p{color:#64748b;font-size:14px;margin:0} 2 2 /*# sourceMappingURL=pro-lock.css.map */ -
media-tracker/trunk/assets/dist/js/mt-admin.js
r3455634 r3457950 1 jQuery(document).ready(function( t){var a=t("#the-list").find('[data-slug="media-tracker"] .deactivate a');a.length&&a.on("click",function(e){e.preventDefault(),t("#mt-feedback-modal").show()}),t("#mt-submit-feedback").on("click",function(){var e=t('textarea[name="feedback"]').val();t.post(mediaTracker.ajax_url,{action:"mt_save_feedback",feedback:e,nonce:mediaTracker.nonce},function(e){e.success?window.location.href=a.attr("href"):alert("There was an error. Please try again.")})}),t("#mt-skip-feedback").on("click",function(){window.location.href=a.attr("href")}),t(window).on("click",function(e){t(e.target).is("#mt-feedback-modal")&&t("#mt-feedback-modal").hide()}),t("#mt-feedback-modal").append('<span class="close">×</span>'),t("#mt-feedback-modal .close").on("click",function(){t("#mt-feedback-modal").hide()}),t(".editinline").on("click",function(e){e.preventDefault();e=t(this).attr("class").split(/\s+/);let a="";e.forEach(function(e){e.startsWith("quick-edit-item-")&&(a=e)}),t(".quick-edit-form").addClass("hidden"),t("."+a).removeClass("hidden")}),t(".quick-edit-form .cancel").on("click",function(){t(this).closest(".quick-edit-form").addClass("hidden")}),t("#clear-broken-links-transient").on("click",function(e){e.preventDefault();var a=t(this);t.ajax({url:mediaTracker.ajax_url,type:"POST",data:{action:"clear_broken_links_transient",nonce:mediaTracker.nonce},beforeSend:function(){a.text("Clearing...").prop("disabled",!0)},success:function(e){e.success&&e.data&&(t("#success-message").text("Transient cache cleared successfully!").fadeIn(),setTimeout(function(){location.reload()},2e3))},complete:function(){a.text("Clear Transient Cache").prop("disabled",!1)}})}),t("#mt-scan-now-btn").on("click",function(e){e.preventDefault();var c=t(this),e=t("#mt-scan-progress-container"),i=t("#mt-scan-progress-bar"),o=t("#mt-scan-progress-text"),r=(c.prop("disabled",!0).text("Scanning..."),e.show(),0),s=0;!function n(){t.post(mediaTracker.ajax_url,{action:"mt_scan_batch",offset:r,nonce:mediaTracker.nonce},function(e){var a,t;e.success?(a=e.data,r=a.offset,(t=0)<(s=a.total)&&(t=Math.round(r/s*100)),i.css("width",(t=100<t?100:t)+"%"),o.text(t+"% ("+r+"/"+s+")"),a.done?(c.text("Scan Complete"),o.text("100% - Scan Complete. Reloading..."),setTimeout(function(){location.reload()},1e3)):n()):(c.prop("disabled",!1).text("Scan Failed"),alert("Error: "+(e.data||"Unknown error")))}).fail(function(){c.prop("disabled",!1).text("Scan Failed"),alert("Request failed. Please try again.")})}()})});1 jQuery(document).ready(function(c){var a=c("#the-list").find('[data-slug="media-tracker"] .deactivate a');a.length&&a.on("click",function(e){e.preventDefault(),c("#mt-feedback-modal").show()}),c("#mt-submit-feedback").on("click",function(){var e=c('textarea[name="feedback"]').val();c.post(mediaTracker.ajax_url,{action:"mt_save_feedback",feedback:e,nonce:mediaTracker.nonce},function(e){e.success?window.location.href=a.attr("href"):alert("There was an error. Please try again.")})}),c("#mt-skip-feedback").on("click",function(){window.location.href=a.attr("href")}),c(window).on("click",function(e){c(e.target).is("#mt-feedback-modal")&&c("#mt-feedback-modal").hide()}),c("#mt-feedback-modal").append('<span class="close">×</span>'),c("#mt-feedback-modal .close").on("click",function(){c("#mt-feedback-modal").hide()}),c(".editinline").on("click",function(e){e.preventDefault();e=c(this).attr("class").split(/\s+/);let a="";e.forEach(function(e){e.startsWith("quick-edit-item-")&&(a=e)}),c(".quick-edit-form").addClass("hidden"),c("."+a).removeClass("hidden")}),c(".quick-edit-form .cancel").on("click",function(){c(this).closest(".quick-edit-form").addClass("hidden")}),c("#clear-broken-links-transient").on("click",function(e){e.preventDefault();var a=c(this);c.ajax({url:mediaTracker.ajax_url,type:"POST",data:{action:"clear_broken_links_transient",nonce:mediaTracker.nonce},beforeSend:function(){a.text("Clearing...").prop("disabled",!0)},success:function(e){e.success&&e.data&&(c("#success-message").text("Transient cache cleared successfully!").fadeIn(),setTimeout(function(){location.reload()},2e3))},complete:function(){a.text("Clear Transient Cache").prop("disabled",!1)}})}),c("#media-duplicate-filter").on("change",function(){c("#post-query-submit").click()}),c("#rescan-duplicates-btn").on("click",function(){var a,n;confirm(mediaTracker.i18n.rescan_confirm||"Image hashes will be refreshed and all images will be re-scanned. Continue?")&&(a=c(this),n=c("#rescan-status"),a.prop("disabled",!0),n.html('<span class="spinner is-active"></span> '+(mediaTracker.i18n.rescanning||"Re-scanning...")).show(),c.ajax({url:mediaTracker.ajax_url,type:"POST",data:{action:"reset_duplicate_hashes",nonce:mediaTracker.nonce},success:function(e){e.success?(n.html(e.data.message),setTimeout(function(){location.reload()},2e3)):(n.html('<span style="color:red;">'+(mediaTracker.i18n.rescan_error||"Error re-scanning images")+"</span>"),a.prop("disabled",!1))},error:function(){n.html('<span style="color:red;">'+(mediaTracker.i18n.rescan_error||"Error re-scanning images")+"</span>"),a.prop("disabled",!1)}}))})}); -
media-tracker/trunk/assets/dist/js/tab.js
r3454648 r3457950 1 ( c=>{window.lucide&&"function"==typeof lucide.createIcons&&lucide.createIcons();var a=document.querySelectorAll("aside nav li"),n=document.querySelectorAll(".tab-content");function s(t){var e;t&&(a.forEach(function(e){e.classList.remove("active")}),n.forEach(function(e){e.classList.remove("active")}),a.forEach(function(e){e.getAttribute("data-tab")===t&&e.classList.add("active")}),e=document.getElementById("tab-"+t))&&e.classList.add("active")}a.forEach(function(r){r.addEventListener("click",function(e){var t=r.getAttribute("data-tab");if(t){e&&e.target&&e.target.tagName&&"a"===e.target.tagName.toLowerCase()&&e.preventDefault(),e.preventDefault();for(var a,n,i=new URL(window.location.href),c=["paged","mt_dup_page","mt_dup_sort","mt_dup_dir","orderby","order","s"],o=!1,d=0;d<c.length;d++)if(i.searchParams.has(c[d])){o=!0;break}o?(e=i.searchParams.get("page")||"media-tracker",(a=new URLSearchParams).set("page",e),a.set("tab",t),window.location.href=i.pathname+"?"+a.toString()):(s(t),e=t,"undefined"!=typeof URL&&window.history&&"function"==typeof window.history.replaceState&&(t=(a=new URL(window.location.href)).searchParams.get("page")||"media-tracker",(n=new URLSearchParams).set("page",t),n.set("tab",e),a.search=n.toString(),window.history.replaceState({},"",a.toString())))}})});var t=!1,i=!1;if(a.forEach(function(e){e.classList.contains("active")&&(t=!0)}),n.forEach(function(e){e.classList.contains("active")&&(i=!0)}),!(t&&i||"undefined"==typeof URL)){var e=new URL(window.location.href),o=e.searchParams.get("tab");if(!o)for(var d=e.search||"",r=["overview","unused-media","duplicates","external-storage","optimization","security","multisite","settings","license"],u=0;u<r.length;u++)if(-1!==d.indexOf(r[u])){o=r[u];break}o&&s(o)}var e=document.getElementById("btn-add-connection"),l=document.getElementById("connection-modal"),m=document.getElementById("connection-modal-close"),f=document.getElementById("connection-modal-cancel"),p=document.getElementById("connection-modal-test"),g=document.getElementById("connection-modal-save");function h(){l&&l.classList.remove("active")}e&&l&&e.addEventListener("click",function(){l&&l.classList.add("active")}),m&&m.addEventListener("click",h),f&&f.addEventListener("click",h),p&&p.addEventListener("click",function(){alert("Test connection successful (demo).")}),g&&g.addEventListener("click",function(){alert("Connection saved (demo)."),h()}),c(function(){function a(e){var t;e.length&&confirm("Are you sure you want to delete the selected duplicate images?")&&(t=window.mediaTracker&&window.mediaTracker.nonce?window.mediaTracker.nonce:"",c.post(window.mediaTracker&&window.mediaTracker.ajax_url||ajaxurl,{action:"mt_delete_duplicate_images",nonce:t,attachment_ids:e}).done(function(e){e&&e.success?location.reload():e&&e.data&&e.data.message?alert(e.data.message):alert("Failed to delete duplicate images.")}).fail(function(){alert("Failed to delete duplicate images.")}))}c("#mt-dup-select-all").on("change",function(){var e=c(this).is(":checked");c("#mt-duplicate-form").find('tbody input[type="checkbox"]').prop("checked",e)}),c("#mt-dup-delete-selected").on("click",function(e){e.preventDefault();var t=[];c("#mt-duplicate-form").find('tbody input[type="checkbox"]:checked').each(function(){t.push(c(this).val())}),a(t)}),c(document).on("click",".mt-dup-delete-single",function(e){e.preventDefault();e=c(this).data("id");e&&a([e])}),c("#mt-dup-scan").on("click",function(e){e.preventDefault();var t,a,n,i=c(this);i.data("running")||confirm("Re-scan all media for duplicates? This may take some time on large libraries.")&&(i.data("running",!0).prop("disabled",!0).text("Scanning..."),c(".mt-dup-wrap").show(),t=c("#mt-dup-progress"),a=t.find(".mt-dup-progress-bar"),n=c(".mt-dup-scan-status"),e=c(".mt-dup-progress-percent"),a.stop(!0).css("width","0%"),e.length&&e.text("0%").show(),a.animate({width:"70%"},{duration:4e3,step:function(e){n.length&&(e=Math.round(e),n.text("Scan status: Scanning... ("+e+"%)"))}}),t.show(),n.show().text("Scan status: Starting... (0%)"),e=window.mediaTracker&&window.mediaTracker.nonce?window.mediaTracker.nonce:"",c.post(window.mediaTracker&&window.mediaTracker.ajax_url||ajaxurl,{action:"reset_duplicate_hashes",nonce:e}).done(function(e){a.stop(!0).animate({width:"100%"},{duration:2e3,step:function(e){n.length&&(e=Math.round(e),n.text("Scan status: Finishing... ("+e+"%)"))},complete:function(){e&&e.success&&e.data&&e.data.message?n.html("✅ <strong>Scan Complete!</strong> - "+e.data.message):n.html("✅ <strong>Scan Complete!</strong> (100%)"),setTimeout(function(){t.fadeOut(300,function(){location.reload()})},1e3)}})}).fail(function(){a.stop(!0).animate({width:"100%"},{duration:1600,step:function(e){n.length&&(e=Math.round(e),n.text("Scan status: Error ("+e+"%)"))},complete:function(){n.html("❌ <strong>Error:</strong> Failed to start scan."),setTimeout(function(){t.fadeOut(300,function(){n.text("Scan status: Ready to scan..."),i.data("running",!1).prop("disabled",!1).text("Scan Duplicates")})},3e3)}})}))})})})(jQuery);1 (s=>{window.lucide&&"function"==typeof lucide.createIcons&&lucide.createIcons();var t,e=document.querySelectorAll("aside nav li"),n=document.querySelectorAll(".tab-content"),a=!1,i=!1;if(e.forEach(function(e){e.classList.contains("active")&&(a=!0)}),n.forEach(function(e){e.classList.contains("active")&&(i=!0)}),!(a&&i||"undefined"==typeof URL)){var c=new URL(window.location.href),o=c.searchParams.get("tab");if(!o)for(var d=c.search||"",r=["overview","unused-media","duplicates","external-storage","optimization","security","multisite","settings","license"],l=0;l<r.length;l++)if(-1!==d.indexOf(r[l])){o=r[l];break}o&&(t=o)&&(e.forEach(function(e){e.classList.remove("active")}),n.forEach(function(e){e.classList.remove("active")}),e.forEach(function(e){e.getAttribute("data-tab")===t&&e.classList.add("active")}),c=document.getElementById("tab-"+t))&&c.classList.add("active")}var n=document.getElementById("btn-add-connection"),u=document.getElementById("connection-modal"),e=document.getElementById("connection-modal-close"),c=document.getElementById("connection-modal-cancel"),m=document.getElementById("connection-modal-test"),f=document.getElementById("connection-modal-save");function p(){u&&u.classList.remove("active")}n&&u&&n.addEventListener("click",function(){u&&u.classList.add("active")}),e&&e.addEventListener("click",p),c&&c.addEventListener("click",p),m&&m.addEventListener("click",function(){alert("Test connection successful (demo).")}),f&&f.addEventListener("click",function(){alert("Connection saved (demo)."),p()}),s(function(){function n(e){var t;e.length&&confirm("Are you sure you want to delete the selected duplicate images?")&&(t=window.mediaTracker&&window.mediaTracker.nonce?window.mediaTracker.nonce:"",s.post(window.mediaTracker&&window.mediaTracker.ajax_url||ajaxurl,{action:"mt_delete_duplicate_images",nonce:t,attachment_ids:e}).done(function(e){e&&e.success?location.reload():e&&e.data&&e.data.message?alert(e.data.message):alert("Failed to delete duplicate images.")}).fail(function(){alert("Failed to delete duplicate images.")}))}s("#mt-dup-select-all").on("change",function(){var e=s(this).is(":checked");s("#mt-duplicate-form").find('tbody input[type="checkbox"]').prop("checked",e)}),s("#mt-dup-delete-selected").on("click",function(e){e.preventDefault();var t=[];s("#mt-duplicate-form").find('tbody input[type="checkbox"]:checked').each(function(){t.push(s(this).val())}),n(t)}),s(document).on("click",".mt-dup-delete-single",function(e){e.preventDefault();e=s(this).data("id");e&&n([e])}),s("#mt-dup-scan").on("click",function(e){e.preventDefault();var t,n,i,c,o,a=s(this);function d(e){c.html("❌ <strong>Error:</strong> "+e),a.data("running",!1).prop("disabled",!1).html(t),s(window).off("beforeunload.mediaTrackerDupScan"),setTimeout(function(){n.fadeOut(300,function(){c.text("Scan status: Ready to scan...")})},3e3)}a.data("running")||confirm("Re-scan all media for duplicates? This may take some time on large libraries.")&&(t=a.html(),a.data("running",!0).prop("disabled",!0),a.html('<span class="spinner is-active" style="float:none; margin:0 5px 0 0; visibility:visible;"></span> Scanning...'),s(".mt-dup-wrap").show(),n=s("#mt-dup-progress"),i=n.find(".mt-dup-progress-bar"),c=s(".mt-dup-scan-status"),i.stop(!0).css("width","0%"),n.show(),c.show().text("Scan status: Starting... (0%)"),o=window.mediaTracker&&window.mediaTracker.nonce?window.mediaTracker.nonce:"",s(window).on("beforeunload.mediaTrackerDupScan",function(){return"Scanning in progress. Please do not close this tab."}),s.post(window.mediaTracker&&window.mediaTracker.ajax_url||ajaxurl,{action:"reset_duplicate_hashes",nonce:o}).done(function(e){e.success?function a(){s.post(window.mediaTracker&&window.mediaTracker.ajax_url||ajaxurl,{action:"mt_process_batch",nonce:o}).done(function(e){var t,n;e.success?(t=e.data,n=t.percentage,i.css("width",n+"%"),c.text("Scan status: Scanning... ("+n+"%)"),t.completed?(c.html("✅ <strong>Scan Complete!</strong> (100%)"),i.css("width","100%"),s(window).off("beforeunload.mediaTrackerDupScan"),setTimeout(function(){location.reload()},1e3)):a()):d(e.data.message||"Error processing batch.")}).fail(function(){d("Network error during scan.")})}():d("Failed to initialize scan.")}).fail(function(){d("Failed to initialize scan.")}))})})})(jQuery); -
media-tracker/trunk/assets/src/js/mt-admin.js
r3455634 r3457950 101 101 }); 102 102 103 // Background Scan Logic (Client-side Batching) 104 $('#mt-scan-now-btn').on('click', function(e) { 105 e.preventDefault(); 106 var button = $(this); 107 var progressContainer = $('#mt-scan-progress-container'); 108 var progressBar = $('#mt-scan-progress-bar'); 109 var progressText = $('#mt-scan-progress-text'); 103 // Media Duplicate Filter - Auto trigger filter on change 104 $('#media-duplicate-filter').on('change', function() { 105 // Trigger the filter button click programmatically 106 $('#post-query-submit').click(); 107 }); 110 108 111 button.prop('disabled', true).text('Scanning...'); 112 progressContainer.show(); 113 114 var offset = 0; 115 var total = 0; 116 117 function processBatch() { 118 $.post(mediaTracker.ajax_url, { 119 action: 'mt_scan_batch', 120 offset: offset, 121 nonce: mediaTracker.nonce 122 }, function(response) { 123 if (response.success) { 124 var data = response.data; 125 offset = data.offset; 126 total = data.total; 127 128 var percent = 0; 129 if (total > 0) { 130 percent = Math.round((offset / total) * 100); 131 } 132 if (percent > 100) percent = 100; 133 134 progressBar.css('width', percent + '%'); 135 progressText.text(percent + '% (' + offset + '/' + total + ')'); 136 137 if (!data.done) { 138 processBatch(); // Process next batch 139 } else { 140 // Done 141 button.text('Scan Complete'); 142 progressText.text('100% - Scan Complete. Reloading...'); 143 setTimeout(function() { 144 location.reload(); 145 }, 1000); 146 } 147 } else { 148 button.prop('disabled', false).text('Scan Failed'); 149 alert('Error: ' + (response.data || 'Unknown error')); 150 } 151 }).fail(function() { 152 button.prop('disabled', false).text('Scan Failed'); 153 alert('Request failed. Please try again.'); 154 }); 109 // Duplicate Images Re-scan Button 110 $('#rescan-duplicates-btn').on('click', function() { 111 if (!confirm(mediaTracker.i18n.rescan_confirm || 'Image hashes will be refreshed and all images will be re-scanned. Continue?')) { 112 return; 155 113 } 156 114 157 // Start the process 158 processBatch(); 115 var button = $(this); 116 var status = $('#rescan-status'); 117 118 button.prop('disabled', true); 119 status.html('<span class="spinner is-active"></span> ' + (mediaTracker.i18n.rescanning || 'Re-scanning...')).show(); 120 121 $.ajax({ 122 url: mediaTracker.ajax_url, 123 type: 'POST', 124 data: { 125 action: 'reset_duplicate_hashes', 126 nonce: mediaTracker.nonce 127 }, 128 success: function(response) { 129 if (response.success) { 130 status.html(response.data.message); 131 setTimeout(function() { 132 location.reload(); 133 }, 2000); 134 } else { 135 status.html('<span style="color:red;">' + (mediaTracker.i18n.rescan_error || 'Error re-scanning images') + '</span>'); 136 button.prop('disabled', false); 137 } 138 }, 139 error: function() { 140 status.html('<span style="color:red;">' + (mediaTracker.i18n.rescan_error || 'Error re-scanning images') + '</span>'); 141 button.prop('disabled', false); 142 } 143 }); 159 144 }); 160 161 145 }); -
media-tracker/trunk/assets/src/js/tab.js
r3454648 r3457950 76 76 } 77 77 } 78 79 // Tab click handlers80 navItems.forEach(function(item) {81 item.addEventListener('click', function(e) {82 var target = item.getAttribute('data-tab');83 if (!target) {84 return;85 }86 if (e && e.target && e.target.tagName && e.target.tagName.toLowerCase() === 'a') {87 e.preventDefault();88 }89 e.preventDefault();90 91 // Check if current URL has pagination or sorting params92 var currentUrl = new URL(window.location.href);93 var dirtyParams = ['paged', 'mt_dup_page', 'mt_dup_sort', 'mt_dup_dir', 'orderby', 'order', 's'];94 var isDirty = false;95 96 for (var i = 0; i < dirtyParams.length; i++) {97 if (currentUrl.searchParams.has(dirtyParams[i])) {98 isDirty = true;99 break;100 }101 }102 103 // If we are on a paginated/sorted view and switching tabs, force reload to reset state104 if (isDirty) {105 var page = currentUrl.searchParams.get('page') || 'media-tracker';106 var newParams = new URLSearchParams();107 newParams.set('page', page);108 newParams.set('tab', target);109 110 window.location.href = currentUrl.pathname + '?' + newParams.toString();111 return;112 }113 114 setActiveTab(target);115 updateUrlForTab(target);116 });117 });118 78 119 79 // Initialize active tab from URL (only if not already set server-side) … … 272 232 } 273 233 274 btn.data('running', true).prop('disabled', true).text('Scanning...'); 234 var originalText = btn.html(); 235 btn.data('running', true).prop('disabled', true); 236 // Add spinner as requested 237 btn.html('<span class="spinner is-active" style="float:none; margin:0 5px 0 0; visibility:visible;"></span> Scanning...'); 275 238 276 239 $('.mt-dup-wrap').show(); … … 279 242 var progressBar = progressWrap.find('.mt-dup-progress-bar'); 280 243 var status = $('.mt-dup-scan-status'); 281 var percent = $('.mt-dup-progress-percent');282 244 283 245 progressBar.stop(true).css('width', '0%'); 284 if (percent.length) {285 percent.text('0%').show();286 }287 288 progressBar.animate(289 { width: '70%' },290 {291 duration: 4000,292 step: function(now) {293 if (status.length) {294 var p = Math.round(now);295 status.text('Scan status: Scanning... (' + p + '%)');296 }297 }298 }299 );300 301 246 progressWrap.show(); 302 247 status.show().text('Scan status: Starting... (0%)'); … … 304 249 var nonce = (window.mediaTracker && window.mediaTracker.nonce) ? window.mediaTracker.nonce : ''; 305 250 251 // Prevent tab closing 252 $(window).on('beforeunload.mediaTrackerDupScan', function() { 253 return 'Scanning in progress. Please do not close this tab.'; 254 }); 255 256 // Step 1: Reset hashes and start 306 257 $.post((window.mediaTracker && window.mediaTracker.ajax_url) || ajaxurl, { 307 258 action: 'reset_duplicate_hashes', 308 259 nonce: nonce 309 260 }).done(function(res) { 310 progressBar.stop(true).animate( 311 { width: '100%' }, 312 { 313 duration: 2000, 314 step: function(now) { 315 if (status.length) { 316 var p = Math.round(now); 317 status.text('Scan status: Finishing... (' + p + '%)'); 318 } 319 }, 320 complete: function() { 321 if (res && res.success && res.data && res.data.message) { 322 status.html('✅ <strong>Scan Complete!</strong> - ' + res.data.message); 323 } else { 324 status.html('✅ <strong>Scan Complete!</strong> (100%)'); 325 } 326 327 // Hide progress bar after 1 second 261 if (res.success) { 262 // Start recursive batch processing 263 processBatch(); 264 } else { 265 handleError('Failed to initialize scan.'); 266 } 267 }).fail(function() { 268 handleError('Failed to initialize scan.'); 269 }); 270 271 function processBatch() { 272 $.post((window.mediaTracker && window.mediaTracker.ajax_url) || ajaxurl, { 273 action: 'mt_process_batch', 274 nonce: nonce 275 }).done(function(res) { 276 if (res.success) { 277 var data = res.data; 278 var pct = data.percentage; 279 280 progressBar.css('width', pct + '%'); 281 status.text('Scan status: Scanning... (' + pct + '%)'); 282 283 if (data.completed) { 284 status.html('✅ <strong>Scan Complete!</strong> (100%)'); 285 progressBar.css('width', '100%'); 286 287 // Remove tab closing prevention 288 $(window).off('beforeunload.mediaTrackerDupScan'); 289 328 290 setTimeout(function() { 329 progressWrap.fadeOut(300, function() { 330 location.reload(); 331 }); 291 location.reload(); 332 292 }, 1000); 293 } else { 294 // Continue processing next batch 295 processBatch(); 333 296 } 297 } else { 298 handleError(res.data.message || 'Error processing batch.'); 334 299 } 335 ); 336 }).fail(function() { 337 progressBar.stop(true).animate( 338 { width: '100%' }, 339 { 340 duration: 1600, 341 step: function(now) { 342 if (status.length) { 343 var p = Math.round(now); 344 status.text('Scan status: Error (' + p + '%)'); 345 } 346 }, 347 complete: function() { 348 status.html('❌ <strong>Error:</strong> Failed to start scan.'); 349 350 // Hide progress bar after 3 seconds on error 351 setTimeout(function() { 352 progressWrap.fadeOut(300, function() { 353 status.text('Scan status: Ready to scan...'); 354 btn.data('running', false).prop('disabled', false).text('Scan Duplicates'); 355 }); 356 }, 3000); 357 } 358 } 359 ); 360 }); 300 }).fail(function() { 301 handleError('Network error during scan.'); 302 }); 303 } 304 305 function handleError(msg) { 306 status.html('❌ <strong>Error:</strong> ' + msg); 307 // Restore button state 308 btn.data('running', false).prop('disabled', false).html(originalText); 309 310 // Remove tab closing prevention 311 $(window).off('beforeunload.mediaTrackerDupScan'); 312 313 // Hide progress bar after delay 314 setTimeout(function() { 315 progressWrap.fadeOut(300, function() { 316 status.text('Scan status: Ready to scan...'); 317 }); 318 }, 3000); 319 } 361 320 }); 362 321 }); -
media-tracker/trunk/assets/src/scss/mt-admin.scss
r3454648 r3457950 146 146 } 147 147 148 #usage_count { 149 width: 140px !important; 150 } 151 148 152 .media-tracker-layout { 149 153 margin: 0; … … 200 204 201 205 li { 202 padding: 13px 10px;203 206 cursor: pointer; 204 207 transition: 0.25s; … … 252 255 } 253 256 254 &.license,255 &.settings,256 &.multisite {257 padding: 15px !important;258 }259 260 &:last-child {261 padding: 0;262 }263 264 257 &:hover { 265 258 a { … … 787 780 padding: 16px 3px; 788 781 } 782 783 td { 784 vertical-align: middle; 785 } 786 787 .mt-dup-delete-single { 788 line-height: normal; 789 padding: 8px 4px; 790 color: #6366f1; 791 border: 1px solid #6366f1; 792 vertical-align: middle; 793 width: 32px; 794 height: 32px; 795 transition: .3s; 796 797 span { 798 font-size: 14px; 799 } 800 801 &:hover { 802 background: #6366f1; 803 color: #fff; 804 } 805 } 789 806 } 790 807 … … 795 812 justify-content: space-between; 796 813 margin-top: 15px; 814 815 .tablenav { 816 margin: 0; 817 padding: 0; 818 } 797 819 } 798 820 … … 801 823 margin: 0 15px; 802 824 } 825 } 826 } 827 828 #tab-duplicates { 829 #mt-dup-scan { 830 background-color: #6366f1; 831 color: #fff; 832 border: none; 833 padding: 4px 14px; 803 834 } 804 835 } … … 831 862 832 863 .mt-overview-table { 833 border-radius: 8px;864 border-radius: 4px; 834 865 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 866 margin-top: 20px; 837 867 width: 100%; … … 839 869 border-spacing: 0; 840 870 background: #fff; 871 border: 1px solid #e2e8f0; 841 872 842 873 thead { … … 854 885 855 886 tr:first-child { 856 th:first-child { border-top-left-radius: 8px; }857 th:last-child { border-top-right-radius: 8px; }887 th:first-child { border-top-left-radius: 4px; } 888 th:last-child { border-top-right-radius: 4px; } 858 889 } 859 890 } … … 936 967 .mt-text-center { text-align: center; } 937 968 .mt-mr-1 { margin-right: 4px; } 938 .mt-thumb-img { width: 60px; height: auto; } 969 .mt-thumb-img { 970 width: 52px; 971 height: 52px; 972 object-fit: cover; 973 border-radius: 4px; 974 } 939 975 .mt-link-clean { text-decoration: none; font-weight: 500; } 940 976 -
media-tracker/trunk/includes/Admin.php
r3455634 r3457950 18 18 new Admin\Duplicate_Images(); 19 19 new Admin\PluginMeta(); 20 new BackgroundProcess();21 20 } 22 21 } -
media-tracker/trunk/includes/Admin/Duplicate_Images.php
r3454648 r3457950 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 add_action('wp_ajax_mt_process_batch', array($this, 'process_batch_via_ajax')); 26 27 add_action('wp_ajax_mt_delete_duplicate_images', array($this, 'delete_duplicate_images_via_ajax')); 27 28 } … … 589 590 delete_transient( 'media_tracker_dashboard_stats_v8' ); 590 591 592 // Save total to scan for progress bar 593 $total_to_scan = $this->count_unhashed_attachments(); 594 update_option('media_tracker_total_to_scan', $total_to_scan); 595 591 596 // Trigger immediate batch processing 592 597 do_action('media_tracker_batch_process'); … … 600 605 __('Reset %1$d hashes. Re-scanning %2$d images...', 'media-tracker'), 601 606 $deleted, 602 $ remaining607 $total_to_scan 603 608 ), 604 609 'deleted' => $deleted, 605 'remaining' => $remaining 610 'remaining' => $remaining, 611 'total' => $total_to_scan 606 612 )); 613 } 614 615 /** 616 * AJAX handler to process a batch of images for duplicate scanning. 617 */ 618 public function process_batch_via_ajax() { 619 if (!current_user_can('manage_options')) { 620 wp_send_json_error(array('message' => __('Unauthorized', 'media-tracker')), 403); 621 } 622 623 check_ajax_referer('media_tracker_nonce', 'nonce'); 624 625 // Run the batch 626 $this->process_image_hashes_batch(); 627 628 // Check status 629 $active = get_option('media_tracker_duplicate_scan_active'); 630 $offset = get_option('media_tracker_offset', 0); 631 $total = get_option('media_tracker_total_to_scan', 0); 632 633 if (!$active) { 634 // Completed 635 wp_send_json_success(array( 636 'completed' => true, 637 'percentage' => 100, 638 'processed' => $total, 639 'total' => $total 640 )); 641 } else { 642 $percentage = ($total > 0) ? min(99, round(($offset / $total) * 100)) : 0; 643 wp_send_json_success(array( 644 'completed' => false, 645 'percentage' => $percentage, 646 'processed' => $offset, 647 'total' => $total 648 )); 649 } 607 650 } 608 651 … … 636 679 if ( $deleted > 0 ) { 637 680 // Clear dashboard stats cache so overview updates 638 delete_transient( 'media_tracker_dashboard_stats_v 6' );681 delete_transient( 'media_tracker_dashboard_stats_v8' ); 639 682 640 683 wp_send_json_success( array( -
media-tracker/trunk/includes/Admin/Media_Usage.php
r3455634 r3457950 24 24 // Column width/styles only on Media Library list screen 25 25 add_action( 'admin_head-upload.php', array( $this, 'print_usage_count_column_css' ) ); 26 // AJAX for dashboard stats 27 add_action( 'wp_ajax_media_tracker_get_most_used', array( $this, 'ajax_get_most_used' ) ); 28 } 29 30 /** 31 * AJAX handler to get most used media. 32 */ 33 public function ajax_get_most_used() { 34 if ( ! current_user_can( 'upload_files' ) ) { 35 wp_send_json_error( 'Permission denied' ); 36 } 37 38 $media_tracker_most_used = self::get_dashboard_most_used_media(); 39 set_transient( 'media_tracker_most_used_media_stats', $media_tracker_most_used, HOUR_IN_SECONDS ); 40 41 ob_start(); 42 if ( ! empty( $media_tracker_most_used ) ) { 43 ?> 44 <table class="mt-overview-table"> 45 <thead> 46 <tr> 47 <th><?php esc_html_e( 'File Name', 'media-tracker' ); ?></th> 48 <th><?php esc_html_e( 'Type', 'media-tracker' ); ?></th> 49 <th><?php esc_html_e( 'Usage Count', 'media-tracker' ); ?></th> 50 <th><?php esc_html_e( 'Actions', 'media-tracker' ); ?></th> 51 </tr> 52 </thead> 53 <tbody> 54 <?php foreach ( $media_tracker_most_used as $media_tracker_media ) : ?> 55 <?php 56 $media_tracker_file_type = $media_tracker_media->post_mime_type ? str_replace( 'image/', '', $media_tracker_media->post_mime_type ) : '-'; 57 $media_tracker_edit_link = get_edit_post_link( $media_tracker_media->ID ); 58 ?> 59 <tr> 60 <td><strong><?php echo esc_html( $media_tracker_media->post_title ); ?></strong></td> 61 <td><?php echo esc_html( $media_tracker_file_type ); ?></td> 62 <td> 63 <span class="tag" style="background: #10b981; color: white;"> 64 <?php 65 /* translators: %d: Number of times the media is used. */ 66 printf( esc_html__( '%d times', 'media-tracker' ), intval( $media_tracker_media->usage_count ) ); 67 ?> 68 </span> 69 </td> 70 <td> 71 <a href="<?php echo esc_url( $media_tracker_edit_link ); ?>" class="btn btn-outline" style="padding: 5px 10px; text-decoration: none;"> 72 <i class="dashicons dashicons-visibility"></i> 73 <?php esc_html_e( 'View', 'media-tracker' ); ?> 74 </a> 75 </td> 76 </tr> 77 <?php endforeach; ?> 78 </tbody> 79 </table> 80 <?php 81 } else { 82 echo '<p style="color: #64748b; text-align: center; padding: 40px;">'; 83 echo '<i class="dashicons dashicons-chart-bar" style="font-size: 48px; color: #cbd5e1; margin-bottom: 15px; height: 48px; width: 48px;"></i><br>'; 84 esc_html_e( 'No media usage data available.', 'media-tracker' ); 85 echo '</p>'; 86 } 87 $html = ob_get_clean(); 88 wp_send_json_success( array( 'html' => $html ) ); 26 89 } 27 90 28 91 /** 29 92 * Get most used media for dashboard stats. 93 * Optimized to scan post content instead of iterating all attachments. 30 94 * 31 95 * @return array Array of objects with ID, post_title, post_mime_type, usage_count. … … 34 98 global $wpdb; 35 99 36 $media_tracker_most_used = array();37 100 $media_tracker_all_usage = array(); 38 101 39 102 // Source 1: Featured images (_thumbnail_id). 103 // This is efficient as it uses an index. 40 104 $media_tracker_featured_media = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 41 105 "SELECT p.ID, p.post_title, p.post_mime_type, COUNT(pm.meta_value) as usage_count … … 61 125 } 62 126 63 // Source 2: Content usage - use media_tracker_index table for efficient lookup 64 // This replaces the N+1 query pattern that made 3 queries per attachment 65 // With the index table, we get all usage in a single query 66 $table_name = $wpdb->prefix . 'media_tracker_index'; 67 68 // Check if index table exists and has data 69 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 70 $index_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name}" ); 71 72 if ( $index_count > 0 ) { 73 // Use the pre-built index table - single query instead of N+1 74 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 75 $content_usage = $wpdb->get_results( 76 "SELECT i.media_id, p.post_title, p.post_mime_type, COUNT(i.id) as usage_count 77 FROM {$table_name} i 78 INNER JOIN {$wpdb->posts} p ON i.media_id = p.ID 79 WHERE p.post_type = 'attachment' 80 AND p.post_status = 'inherit' 81 GROUP BY i.media_id, p.post_title, p.post_mime_type 82 ORDER BY usage_count DESC" 127 // Source 2: Content usage - Scan posts instead of attachments. 128 // We process posts in chunks to avoid memory issues. 129 $limit = 500; 130 $offset = 0; 131 $id_counts = array(); 132 133 // Only scan published posts, pages, and other public types 134 $post_types = array( 'post', 'page' ); 135 $post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) ); 136 137 while ( true ) { 138 $query_args = array_merge( $post_types, array( $limit, $offset ) ); 139 $sql = "SELECT ID, post_content FROM {$wpdb->posts} 140 WHERE post_type IN ($post_types_placeholders) 141 AND post_status = 'publish' 142 LIMIT %d OFFSET %d"; 143 144 $posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 145 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic placeholders. 146 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic SQL. 147 $query_args 148 ) 83 149 ); 84 150 85 foreach ( $content_usage as $usage_item ) { 86 $attachment_id = intval( $usage_item->media_id ); 87 if ( ! isset( $media_tracker_all_usage[ $attachment_id ] ) ) { 88 $media_tracker_all_usage[ $attachment_id ] = array( 89 'ID' => $attachment_id, 90 'post_title' => $usage_item->post_title, 91 'post_mime_type' => $usage_item->post_mime_type, 92 'usage_count' => 0, 93 ); 94 } 95 $media_tracker_all_usage[ $attachment_id ]['usage_count'] += intval( $usage_item->usage_count ); 151 if ( empty( $posts ) ) { 152 break; 153 } 154 155 foreach ( $posts as $post ) { 156 $content = $post->post_content; 157 if ( empty( $content ) ) { 158 continue; 159 } 160 161 // Match wp-image-ID class 162 if ( preg_match_all( '/wp-image-(\d+)/', $content, $matches ) ) { 163 foreach ( $matches[1] as $id ) { 164 $id = intval( $id ); 165 $id_counts[ $id ] = ( isset( $id_counts[ $id ] ) ? $id_counts[ $id ] : 0 ) + 1; 166 } 167 } 168 169 // Match Gutenberg blocks with "id":ID 170 if ( preg_match_all( '/"id":(\d+)/', $content, $matches ) ) { 171 foreach ( $matches[1] as $id ) { 172 $id = intval( $id ); 173 // Basic check to avoid false positives (e.g. small numbers that might not be attachment IDs) 174 // We will verify existence later. 175 $id_counts[ $id ] = ( isset( $id_counts[ $id ] ) ? $id_counts[ $id ] : 0 ) + 1; 176 } 177 } 178 179 // Match gallery shortcodes [gallery ids="1,2,3"] 180 if ( preg_match_all( '/ids=\"([^\"]+)\"/', $content, $matches ) ) { 181 foreach ( $matches[1] as $ids_str ) { 182 $ids = explode( ',', $ids_str ); 183 foreach ( $ids as $id ) { 184 $id = intval( trim( $id ) ); 185 if ( $id > 0 ) { 186 $id_counts[ $id ] = ( isset( $id_counts[ $id ] ) ? $id_counts[ $id ] : 0 ) + 1; 187 } 188 } 189 } 190 } 191 } 192 193 $offset += $limit; 194 195 // Safety break for very large sites to prevent timeout 196 if ( $offset > 10000 ) { 197 break; 198 } 199 } 200 201 // Process gathered counts 202 if ( ! empty( $id_counts ) ) { 203 // Get details for these attachments to verify they exist and get titles 204 $found_ids = array_keys( $id_counts ); 205 206 // Chunk the ID lookup 207 $id_chunks = array_chunk( $found_ids, 500 ); 208 209 foreach ( $id_chunks as $chunk ) { 210 $ids_placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) ); 211 $sql = "SELECT ID, post_title, post_mime_type 212 FROM {$wpdb->posts} 213 WHERE ID IN ($ids_placeholders) 214 AND post_type = 'attachment'"; 215 216 $attachments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 217 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders. 218 $sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic SQL. 219 $chunk 220 ) 221 ); 222 223 foreach ( $attachments as $att ) { 224 $id = $att->ID; 225 $count = isset( $id_counts[ $id ] ) ? $id_counts[ $id ] : 0; 226 227 if ( ! isset( $media_tracker_all_usage[ $id ] ) ) { 228 $media_tracker_all_usage[ $id ] = array( 229 'ID' => $id, 230 'post_title' => $att->post_title, 231 'post_mime_type' => $att->post_mime_type, 232 'usage_count' => 0, 233 ); 234 } 235 $media_tracker_all_usage[ $id ]['usage_count'] += $count; 236 } 96 237 } 97 238 } 98 239 99 240 // Convert to object array and sort by usage. 241 $media_tracker_most_used = array(); 100 242 foreach ( $media_tracker_all_usage as $media_tracker_usage ) { 101 243 $media_tracker_obj = new \stdClass(); … … 383 525 384 526 $results = array(); 385 $table_name = $wpdb->prefix . 'media_tracker_index';386 387 // OPTIMIZATION: Use pre-built index table for fast lookup388 // This avoids expensive LIKE '%...%' queries that cause full table scans389 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching390 $index_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" );391 392 if ( $index_exists ) {393 // Fast path: Get usage info from index table (single indexed query)394 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching395 $indexed_usage = $wpdb->get_results(396 $wpdb->prepare(397 "SELECT DISTINCT i.source_post_id, p.post_title, p.post_type398 FROM {$table_name} i399 INNER JOIN {$wpdb->posts} p ON i.source_post_id = p.ID400 WHERE i.media_id = %d401 AND p.post_status = 'publish'",402 $attachment_id403 )404 );405 406 if ( ! empty( $indexed_usage ) ) {407 foreach ( $indexed_usage as $usage ) {408 $obj = new \stdClass();409 $obj->ID = $usage->source_post_id;410 $obj->post_title = $usage->post_title;411 $obj->post_type = $usage->post_type;412 $results[] = $obj;413 }414 415 // Also check site icon (not in index)416 $site_icon_id = get_option( 'site_icon' );417 if ( intval( $site_icon_id ) === intval( $attachment_id ) ) {418 $site_icon_result = new \stdClass();419 $site_icon_result->ID = 0;420 $site_icon_result->post_title = __( 'Site Icon (Favicon)', 'media-tracker' );421 $site_icon_result->post_type = 'site_icon';422 array_unshift( $results, $site_icon_result );423 }424 425 // Cache for 5 minutes426 wp_cache_set( $cache_key, $results, 'media_tracker', 300 );427 return $results;428 }429 }430 431 // Fallback: Run traditional queries if index is empty or doesn't exist432 527 433 528 // Check if the image is used as site icon … … 592 687 if ( preg_match_all( '/https?:\/\/[^"\']+\/wp-content\/uploads\/[^"\']+/i', $p->post_content, $m ) ) { 593 688 foreach ( $m[0] as $url ) { 594 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url( $url );689 $id = attachment_url_to_postid( $url ); 595 690 if ( $id && intval( $id ) === intval( $attachment_id ) ) { 596 691 $results[] = (object) array( 'ID' => $p->ID, 'post_title' => $p->post_title, 'post_type' => $p->post_type ); … … 650 745 if ( preg_match_all( '/\\b(src|url|image_url|background_image)\\s*=\\s*\"([^\"]+)\"/i', $c, $m_urls ) ) { 651 746 foreach ( $m_urls[2] as $url ) { 652 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url( $url );747 $id = attachment_url_to_postid( $url ); 653 748 if ( $id && intval( $id ) === intval( $attachment_id ) ) { 654 749 $found = true; -
media-tracker/trunk/includes/Admin/Menu.php
r3455634 r3457950 101 101 */ 102 102 public function register_media_tracker_menu() { 103 // Main parent page - will show overview by default 103 104 add_media_page( 104 105 __( 'Media Tracker', 'media-tracker' ), … … 108 109 array( $this, 'media_tracker_admin_page' ) 109 110 ); 111 112 // Add submenu pages for each tab 113 add_submenu_page( 114 null, // Parent slug - null to hide from sidebar menu 115 __( 'Dashboard', 'media-tracker' ), 116 __( 'Dashboard', 'media-tracker' ), 117 'manage_options', 118 'media-tracker-overview', 119 array( $this, 'media_tracker_overview_page' ) 120 ); 121 122 add_submenu_page( 123 null, 124 __( 'Unused Media', 'media-tracker' ), 125 __( 'Unused Media', 'media-tracker' ), 126 'manage_options', 127 'media-tracker-unused-media', 128 array( $this, 'media_tracker_unused_media_page' ) 129 ); 130 131 add_submenu_page( 132 null, 133 __( 'Duplicate Media', 'media-tracker' ), 134 __( 'Duplicate Media', 'media-tracker' ), 135 'manage_options', 136 'media-tracker-duplicates', 137 array( $this, 'media_tracker_duplicates_page' ) 138 ); 139 140 add_submenu_page( 141 null, 142 __( 'External Storage', 'media-tracker' ), 143 __( 'External Storage', 'media-tracker' ), 144 'manage_options', 145 'media-tracker-external-storage', 146 array( $this, 'media_tracker_external_storage_page' ) 147 ); 148 149 add_submenu_page( 150 null, 151 __( 'Optimization', 'media-tracker' ), 152 __( 'Optimization', 'media-tracker' ), 153 'manage_options', 154 'media-tracker-optimization', 155 array( $this, 'media_tracker_optimization_page' ) 156 ); 157 158 add_submenu_page( 159 null, 160 __( 'Security & Logs', 'media-tracker' ), 161 __( 'Security & Logs', 'media-tracker' ), 162 'manage_options', 163 'media-tracker-security', 164 array( $this, 'media_tracker_security_page' ) 165 ); 166 167 add_submenu_page( 168 null, 169 __( 'Multi-site', 'media-tracker' ), 170 __( 'Multi-site', 'media-tracker' ), 171 'manage_options', 172 'media-tracker-multisite', 173 array( $this, 'media_tracker_multisite_page' ) 174 ); 175 176 add_submenu_page( 177 null, 178 __( 'Documents', 'media-tracker' ), 179 __( 'Documents', 'media-tracker' ), 180 'manage_options', 181 'media-tracker-documents', 182 array( $this, 'media_tracker_documents_page' ) 183 ); 184 185 if ( media_tracker_is_pro_active() ) { 186 add_submenu_page( 187 null, 188 __( 'License', 'media-tracker' ), 189 __( 'License', 'media-tracker' ), 190 'manage_options', 191 'media-tracker-license', 192 array( $this, 'media_tracker_license_page' ) 193 ); 194 } 110 195 } 111 196 112 197 public function media_tracker_admin_page() { 198 // Set the default tab to overview for main page 199 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for determining the default tab. 200 if ( ! isset( $_GET['tab'] ) ) { 201 $_GET['tab'] = 'overview'; 202 } 203 include __DIR__ . '/views/media-tracker.php'; 204 } 205 206 public function media_tracker_overview_page() { 207 $this->render_tab_page( 'overview' ); 208 } 209 210 public function media_tracker_unused_media_page() { 211 $this->render_tab_page( 'unused-media' ); 212 } 213 214 public function media_tracker_duplicates_page() { 215 $this->render_tab_page( 'duplicates' ); 216 } 217 218 public function media_tracker_external_storage_page() { 219 $this->render_tab_page( 'external-storage' ); 220 } 221 222 public function media_tracker_optimization_page() { 223 $this->render_tab_page( 'optimization' ); 224 } 225 226 public function media_tracker_security_page() { 227 $this->render_tab_page( 'security' ); 228 } 229 230 public function media_tracker_multisite_page() { 231 $this->render_tab_page( 'multisite' ); 232 } 233 234 public function media_tracker_documents_page() { 235 $this->render_tab_page( 'documents' ); 236 } 237 238 public function media_tracker_license_page() { 239 $this->render_tab_page( 'license' ); 240 } 241 242 /** 243 * Render individual tab page 244 * 245 * @param string $tab Tab slug to render 246 */ 247 private function render_tab_page( $tab ) { 248 // Set the current tab 249 $_GET['tab'] = $tab; 250 251 // Include the main layout which will only load the requested tab 113 252 include __DIR__ . '/views/media-tracker.php'; 114 253 } … … 236 375 237 376 // Indicate scanning 238 $progress['current_step'] = 'Scanning posts for used media...';377 $progress['current_step'] = 'Scanning...'; 239 378 set_transient( $progress_key, $progress, 1800 ); 240 379 241 // Step 1: Clear the index table and populate it with used media using ScanEngine 242 global $wpdb; 243 $table_name = $wpdb->prefix . 'media_tracker_index'; 244 245 // Clear existing index 246 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 247 $wpdb->query( "TRUNCATE TABLE $table_name" ); 248 249 // Use ScanEngine to populate the index table 250 $scan_engine = new \Media_Tracker\ScanEngine(); 251 252 // Get all posts to scan 253 $post_types = $scan_engine->get_scannable_post_types(); 254 $total_posts = count( get_posts( array( 255 'post_type' => $post_types, 256 'post_status' => array( 'publish', 'pending', 'draft', 'future', 'private', 'inherit' ), 257 'fields' => 'ids', 258 'posts_per_page' => -1, 259 ) ) ); 260 261 // Scan in batches 262 $batch_size = 50; 263 $offset = 0; 264 $scanned_count = 0; 265 266 while ( $scanned_count < $total_posts ) { 267 $processed = $scan_engine->scan_batch( $batch_size, $offset ); 268 if ( 0 === $processed ) { 269 break; 270 } 271 $scanned_count += $processed; 272 $offset += $processed; 273 274 // Update progress periodically 275 if ( $scanned_count % 100 === 0 ) { 276 $progress['current_step'] = "Scanned $scanned_count/$total_posts posts..."; 277 set_transient( $progress_key, $progress, 1800 ); 278 } 279 } 280 281 // Step 2: Now create the snapshot of unused media 282 $progress['current_step'] = 'Creating unused media snapshot...'; 283 set_transient( $progress_key, $progress, 1800 ); 284 380 // Run the scan synchronously 285 381 $list = new Unused_Media_List( '', null ); 286 287 382 if ( method_exists( $list, 'force_clear_cache' ) ) { 288 383 $list->force_clear_cache(); … … 291 386 // Use new method to scan and save snapshot 292 387 if ( method_exists( $list, 'scan_and_save_snapshot' ) ) { 293 $ unused_count = $list->scan_and_save_snapshot();388 $list->scan_and_save_snapshot(); 294 389 } else { 295 390 // Fallback for safety 296 391 $list->prepare_items(); 297 $unused_count = 0; 298 } 299 300 // Update stats 301 $scan_engine->update_stats(); 392 } 302 393 303 394 // Mark complete … … 305 396 'step' => 6, 306 397 'total_steps' => 6, 307 'current_step' => "Scan complete. Found $unused_count unused media items."398 'current_step' => 'Scan complete' 308 399 ), 1800 ); 309 400 … … 334 425 } 335 426 427 // Perform the scan 428 $list = new Unused_Media_List( '', null ); 429 336 430 // Update progress state before heavy work 337 431 $progress_key = 'media_scan_progress_' . $user_id; … … 340 434 $progress = array( 'step' => 0, 'total_steps' => 6, 'current_step' => 'Starting scan...', 'used_ids' => array() ); 341 435 } 342 $progress['current_step'] = 'Scanning posts for used media...';436 $progress['current_step'] = 'Scanning...'; 343 437 set_transient( $progress_key, $progress, 1800 ); 344 345 // Step 1: Clear the index table and populate it with used media using ScanEngine346 global $wpdb;347 $table_name = $wpdb->prefix . 'media_tracker_index';348 349 // Clear existing index350 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching351 $wpdb->query( "TRUNCATE TABLE $table_name" );352 353 // Use ScanEngine to populate the index table354 $scan_engine = new \Media_Tracker\ScanEngine();355 356 // Get all posts to scan357 $post_types = $scan_engine->get_scannable_post_types();358 $total_posts = count( get_posts( array(359 'post_type' => $post_types,360 'post_status' => array( 'publish', 'pending', 'draft', 'future', 'private', 'inherit' ),361 'fields' => 'ids',362 'posts_per_page' => -1,363 ) ) );364 365 // Scan in batches366 $batch_size = 50;367 $offset = 0;368 $scanned_count = 0;369 370 while ( $scanned_count < $total_posts ) {371 $processed = $scan_engine->scan_batch( $batch_size, $offset );372 if ( 0 === $processed ) {373 break;374 }375 $scanned_count += $processed;376 $offset += $processed;377 378 // Update progress periodically379 if ( $scanned_count % 100 === 0 ) {380 $progress['current_step'] = "Scanned $scanned_count/$total_posts posts...";381 set_transient( $progress_key, $progress, 1800 );382 }383 }384 385 // Step 2: Now create the snapshot of unused media386 $progress['current_step'] = 'Creating unused media snapshot...';387 set_transient( $progress_key, $progress, 1800 );388 389 $list = new Unused_Media_List( '', null );390 438 391 439 if ( method_exists( $list, 'force_clear_cache' ) ) { … … 395 443 // Use new method to scan and save snapshot 396 444 if ( method_exists( $list, 'scan_and_save_snapshot' ) ) { 397 $ unused_count = $list->scan_and_save_snapshot();445 $list->scan_and_save_snapshot(); 398 446 } else { 399 447 // Fallback for safety 400 448 $list->prepare_items(); 401 $unused_count = 0; 402 } 403 404 // Update stats 405 $scan_engine->update_stats(); 449 } 406 450 407 451 // Mark progress as complete … … 409 453 'step' => 6, 410 454 'total_steps' => 6, 411 'current_step' => "Scan complete. Found $unused_count unused media items."455 'current_step' => 'Scan complete' 412 456 ), 1800 ); 413 457 } -
media-tracker/trunk/includes/Admin/Unused_Media_List.php
r3455634 r3457950 3 3 namespace Media_Tracker\Admin; 4 4 5 defined( 'ABSPATH') || exit;5 defined( 'ABSPATH' ) || exit; 6 6 7 7 use WP_List_Table; … … 10 10 * Custom WP_List_Table implementation for managing unused media attachments. 11 11 */ 12 class Unused_Media_List extends WP_List_Table 13 { 12 class Unused_Media_List extends WP_List_Table { 14 13 15 14 /** … … 33 32 * @param int|null $author_id Optional. Author ID to filter media by author. 34 33 */ 35 public function __construct($search, $author_id = null) 36 { 37 parent::__construct(array( 34 public function __construct( $search, $author_id = null ) { 35 parent::__construct( array( 38 36 'singular' => 'media', 39 'plural' => 'media',40 'ajax' => false,41 ));42 43 $this->search = $search;37 'plural' => 'media', 38 'ajax' => false, 39 ) ); 40 41 $this->search = $search; 44 42 $this->author_id = $author_id; 45 43 $this->process_bulk_action(); … … 51 49 * @return array 52 50 */ 53 public function get_columns() 54 { 51 public function get_columns() { 55 52 return array( 56 'cb' => '<input type="checkbox" />',57 'post_title' => __('File', 'media-tracker'),58 'post_author' => __( 'Author', 'media-tracker'),59 'size' => __('Size', 'media-tracker'),60 'post_date' => __('Date', 'media-tracker'),53 'cb' => '<input type="checkbox" />', 54 'post_title' => __( 'File', 'media-tracker' ), 55 'post_author' => __( 'Author', 'media-tracker' ), 56 'size' => __( 'Size', 'media-tracker' ), 57 'post_date' => __( 'Date', 'media-tracker' ), 61 58 ); 62 59 } … … 69 66 * @return string HTML output for the column. 70 67 */ 71 protected function column_default($item, $column_name) 72 { 73 switch ($column_name) { 68 protected function column_default( $item, $column_name ) { 69 switch ( $column_name ) { 74 70 case 'post_title': 75 $edit_link = get_edit_post_link($item->ID);76 $delete_link = get_delete_post_link($item->ID, '', true);77 $view_link = wp_get_attachment_url($item->ID);78 $thumbnail = wp_get_attachment_image($item->ID, [60, 60], true);79 $file_path = get_attached_file($item->ID);80 $file_name = $item->post_title;81 $file_extension = $file_path ? pathinfo( $file_path, PATHINFO_EXTENSION) : '';71 $edit_link = get_edit_post_link( $item->ID ); 72 $delete_link = get_delete_post_link( $item->ID, '', true ); 73 $view_link = wp_get_attachment_url( $item->ID ); 74 $thumbnail = wp_get_attachment_image( $item->ID, [ 60, 60 ], true ); 75 $file_path = get_attached_file( $item->ID ); 76 $file_name = $item->post_title; 77 $file_extension = $file_path ? pathinfo( $file_path, PATHINFO_EXTENSION ) : ''; 82 78 $full_file_name = $file_name . '.' . $file_extension; 83 79 84 80 // Define row actions 85 81 $actions = [ 86 'edit' => '<span class="edit"><a href="' . esc_url($edit_link) . '">' . __('Edit', 'media-tracker') . '</a></span>',87 'view' => '<span class="view"><a href="' . esc_url($view_link) . '">' . __('View', 'media-tracker') . '</a></span>',88 'delete' => '<span class="delete"><a href="' . esc_url($delete_link) . '" class="submitdelete">' . __('Delete Permanently', 'media-tracker') . '</a></span>'82 'edit' => '<span class="edit"><a href="' . esc_url( $edit_link ) . '">' . __( 'Edit', 'media-tracker' ) . '</a></span>', 83 'view' => '<span class="view"><a href="' . esc_url( $view_link ) . '">' . __( 'View', 'media-tracker' ) . '</a></span>', 84 'delete' => '<span class="delete"><a href="' . esc_url( $delete_link ) . '" class="submitdelete">' . __( 'Delete Permanently', 'media-tracker' ) . '</a></span>' 89 85 ]; 90 86 91 87 /* translators: %s: post title */ 92 $aria_label = sprintf( __('"%s" (Edit)', 'media-tracker'), $item->post_title);88 $aria_label = sprintf( __( '"%s" (Edit)', 'media-tracker' ), $item->post_title ); 93 89 94 90 $output = '<strong class="has-media-icon"> 95 <a href="' . esc_url( $edit_link) . '" aria-label="' . esc_attr($aria_label) . '">91 <a href="' . esc_url( $edit_link ) . '" aria-label="' . esc_attr( $aria_label ) . '"> 96 92 <span class="media-icon image-icon">' . $thumbnail . '</span> 97 ' . esc_html( $file_name) . '93 ' . esc_html( $file_name ) . ' 98 94 </a> 99 95 </strong> 100 96 <p class="filename"> 101 <span class="screen-reader-text">' . __( 'File name:', 'media-tracker') . '</span>102 ' . esc_html( $full_file_name) . '97 <span class="screen-reader-text">' . __( 'File name:', 'media-tracker' ) . '</span> 98 ' . esc_html( $full_file_name ) . ' 103 99 </p>'; 104 100 105 101 // Append row actions 106 $output .= $this->row_actions( $actions);102 $output .= $this->row_actions( $actions ); 107 103 return $output; 108 104 case 'post_author': 109 $author_name = get_the_author_meta( 'display_name', $item->post_author);110 $author_url = add_query_arg(105 $author_name = get_the_author_meta( 'display_name', $item->post_author ); 106 $author_url = add_query_arg( 111 107 [ 112 'page' => 'media-tracker',108 'page' => 'media-tracker', 113 109 'author' => $item->post_author, 114 110 ], 115 admin_url( 'upload.php')111 admin_url( 'upload.php' ) 116 112 ); 117 return '<a href="' . esc_url( $author_url) . '">' . esc_html($author_name) . '</a>';113 return '<a href="' . esc_url( $author_url ) . '">' . esc_html( $author_name ) . '</a>'; 118 114 case 'size': 119 $file_path = get_attached_file( $item->ID);120 if ( $file_path && file_exists($file_path)) {121 return size_format( filesize($file_path));115 $file_path = get_attached_file( $item->ID ); 116 if ( $file_path && file_exists( $file_path ) ) { 117 return size_format( filesize( $file_path ) ); 122 118 } else { 123 119 return '-'; 124 120 } 125 121 case 'post_date': 126 return date_i18n( 'Y/m/d', strtotime($item->post_date));122 return date_i18n( 'Y/m/d', strtotime( $item->post_date ) ); 127 123 default: 128 return isset($item->$column_name) ? esc_html($item->$column_name) : ''; 129 } 130 } 131 132 protected function get_sortable_columns() 133 { 124 return isset( $item->$column_name ) ? esc_html( $item->$column_name ) : ''; 125 } 126 } 127 128 protected function get_sortable_columns() { 134 129 return array( 135 'post_title' => array('post_title', false),136 'post_author' => array( 'post_author', false),137 'post_date' => array('post_date', false),138 'size' => array('size', false),130 'post_title' => array( 'post_title', false ), 131 'post_author' => array( 'post_author', false ), 132 'post_date' => array( 'post_date', false ), 133 'size' => array( 'size', false ), 139 134 ); 140 135 } 141 136 142 protected function get_items_per_page($option, $default = 10) 143 { 144 $per_page = (int) get_user_meta(get_current_user_id(), $option, true); 145 return empty($per_page) || $per_page < 1 ? $default : $per_page; 146 } 147 148 /** 149 * Get used media IDs from index table. 150 * 151 * @deprecated Use SQL LEFT JOIN in scan_and_save_snapshot() instead. 152 * This function is no longer called - keeping for backwards compatibility. 153 * Loading 8M IDs into memory is avoided by using SQL-level set operations. 154 * 155 * @return array Array of used media IDs. 156 */ 157 private function get_used_media_ids() 158 { 159 global $wpdb; 160 $table_name = $wpdb->prefix . 'media_tracker_index'; 161 162 // Check if table exists 163 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 164 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { 165 return array(); 166 } 167 168 // NOTE: This query is no longer used in the main scan flow. 169 // scan_and_save_snapshot() now uses SQL LEFT JOIN to find unused IDs 170 // directly in the database without loading all IDs into PHP memory. 171 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 172 $used_ids = $wpdb->get_col("SELECT DISTINCT media_id FROM $table_name"); 173 174 if (empty($used_ids)) { 175 return array(); 176 } 177 178 return array_map('intval', $used_ids); 179 } 180 181 private function get_used_media_ids_deprecated() 182 { 137 protected function get_items_per_page( $option, $default = 10 ) { 138 $per_page = (int) get_user_meta( get_current_user_id(), $option, true ); 139 return empty( $per_page ) || $per_page < 1 ? $default : $per_page; 140 } 141 142 private function get_used_media_ids() { 183 143 global $wpdb; 184 144 … … 187 147 // Check if we have a cached progress 188 148 $progress_key = 'media_scan_progress_' . get_current_user_id(); 189 $progress = get_transient( $progress_key);149 $progress = get_transient( $progress_key ); 190 150 191 151 // Ensure progress structure exists and is valid 192 if ( !is_array($progress)) {152 if ( ! is_array( $progress ) ) { 193 153 $progress = array( 194 154 'step' => 0, … … 198 158 ); 199 159 } else { 200 $progress['step'] = isset( $progress['step']) ? (int) $progress['step'] : 0;201 $progress['total_steps'] = isset( $progress['total_steps']) ? (int) $progress['total_steps'] : 6;202 $progress['current_step'] = isset( $progress['current_step']) ? (string) $progress['current_step'] : 'Starting scan...';203 if ( !isset($progress['used_ids']) || !is_array($progress['used_ids'])) {160 $progress['step'] = isset( $progress['step'] ) ? (int) $progress['step'] : 0; 161 $progress['total_steps'] = isset( $progress['total_steps'] ) ? (int) $progress['total_steps'] : 6; 162 $progress['current_step'] = isset( $progress['current_step'] ) ? (string) $progress['current_step'] : 'Starting scan...'; 163 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) { 204 164 $progress['used_ids'] = array(); 205 165 } 206 166 } 207 167 208 wp_raise_memory_limit( 'admin');209 if ( function_exists('set_time_limit')) {168 wp_raise_memory_limit( 'admin' ); 169 if ( function_exists( 'set_time_limit' ) ) { 210 170 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Scanning can take a long time. 211 set_time_limit( 300);171 set_time_limit( 300 ); 212 172 } 213 173 214 174 // Step 1: Featured Images 215 if ( $progress['step'] < 1) {175 if ( $progress['step'] < 1 ) { 216 176 $progress['current_step'] = 'Scanning featured images...'; 217 set_transient( $progress_key, $progress, 300);177 set_transient( $progress_key, $progress, 300 ); 218 178 219 179 $featured_images = $this->get_cached_db_result(" … … 224 184 AND meta_value != '' 225 185 "); 226 if ( $featured_images) {227 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map('intval', $featured_images));186 if ( $featured_images ) { 187 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $featured_images ) ); 228 188 } 229 189 $progress['step'] = 1; … … 231 191 232 192 // Step 2: WooCommerce Images 233 if ( $progress['step'] < 2 && class_exists('WooCommerce')) {193 if ( $progress['step'] < 2 && class_exists( 'WooCommerce' ) ) { 234 194 $progress['current_step'] = 'Scanning WooCommerce images...'; 235 set_transient( $progress_key, $progress, 300);195 set_transient( $progress_key, $progress, 300 ); 236 196 237 197 $gallery_images = $this->get_cached_db_result(" … … 243 203 "); 244 204 245 foreach ( $gallery_images as $gallery_string) {246 if ( !empty($gallery_string)) {247 $gallery_ids = explode( ',', $gallery_string);248 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map('intval', array_filter($gallery_ids)));205 foreach ( $gallery_images as $gallery_string ) { 206 if ( ! empty( $gallery_string ) ) { 207 $gallery_ids = explode( ',', $gallery_string ); 208 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', array_filter( $gallery_ids ) ) ); 249 209 } 250 210 } … … 261 221 AND meta_value != '' 262 222 "); 263 if ( $variation_images) {264 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map('intval', $variation_images));223 if ( $variation_images ) { 224 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $variation_images ) ); 265 225 } 266 226 $progress['step'] = 2; … … 277 237 AND meta_value != '' 278 238 "); 279 if ( $featured_images) {280 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $featured_images));281 } 282 283 if ( class_exists('WooCommerce')) {239 if ( $featured_images ) { 240 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $featured_images ) ); 241 } 242 243 if ( class_exists( 'WooCommerce' ) ) { 284 244 $gallery_images = $this->get_cached_db_result(" 285 245 SELECT DISTINCT meta_value … … 290 250 "); 291 251 292 foreach ( $gallery_images as $gallery_string) {293 if ( !empty($gallery_string)) {294 $gallery_ids = explode( ',', $gallery_string);295 $used_image_ids = array_merge( $used_image_ids, array_map('intval', array_filter($gallery_ids)));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 ) ) ); 296 256 } 297 257 } … … 308 268 AND meta_value != '' 309 269 "); 310 if ( $variation_images) {311 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $variation_images));312 } 313 } 314 315 if ( class_exists('ACF')) {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' ) ) { 316 276 $acf_meta_values = $this->get_cached_db_result(" 317 277 SELECT DISTINCT pm.meta_value … … 328 288 "); 329 289 330 foreach ( $acf_meta_values as $meta_value) {331 if ( empty($meta_value)) {290 foreach ( $acf_meta_values as $meta_value ) { 291 if ( empty( $meta_value ) ) { 332 292 continue; 333 293 } 334 294 335 if ( is_numeric($meta_value) && $meta_value > 0) {336 $used_image_ids[] = intval( $meta_value);295 if ( is_numeric( $meta_value ) && $meta_value > 0 ) { 296 $used_image_ids[] = intval( $meta_value ); 337 297 continue; 338 298 } 339 299 340 if (is_serialized($meta_value)) { 341 $unserialized = @unserialize($meta_value); 342 if (is_array($unserialized)) { 343 array_walk_recursive($unserialized, function ($item) use (&$used_image_ids) { 344 if (is_numeric($item) && $item > 0 && $item < 999999999) { 345 $used_image_ids[] = intval($item); 300 if ( is_serialized( $meta_value ) ) { 301 $unserialized = @unserialize( $meta_value ); 302 if ( is_array( $unserialized ) ) { 303 array_walk_recursive( $unserialized, function( $item, $key ) use ( &$used_image_ids ) { 304 // Skip common non-ID numeric keys found in attachment metadata and other places 305 if ( in_array( $key, ['width', 'height', 'file_size', 'filesize', 'size', 'length', 'bitrate', 'duration', 'uploaded', 'year', 'month', 'day', 'percent', 'bytes', 'time', 'lossy', 'keep_exif', 'api_version', 'size_before', 'size_after'], true ) ) { 306 return; 307 } 308 309 if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) { 310 $used_image_ids[] = intval( $item ); 346 311 } 347 312 }); … … 350 315 } 351 316 352 if (strpos($meta_value, '"') !== false) { 353 $json_decoded = json_decode($meta_value, true); 354 if (is_array($json_decoded)) { 355 array_walk_recursive($json_decoded, function ($item) use (&$used_image_ids) { 356 if (is_numeric($item) && $item > 0 && $item < 999999999) { 357 $used_image_ids[] = intval($item); 317 if ( strpos( $meta_value, '"' ) !== false ) { 318 $json_decoded = json_decode( $meta_value, true ); 319 if ( is_array( $json_decoded ) ) { 320 array_walk_recursive( $json_decoded, function( $item, $key ) use ( &$used_image_ids ) { 321 // Skip common non-ID numeric keys found in attachment metadata and other places 322 if ( in_array( $key, ['width', 'height', 'file_size', 'filesize', 'size', 'length', 'bitrate', 'duration', 'uploaded', 'year', 'month', 'day'], true ) ) { 323 return; 324 } 325 326 if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) { 327 $used_image_ids[] = intval( $item ); 358 328 } 359 329 }); … … 362 332 } 363 333 364 if ( strpos($meta_value, ',') !== false) {365 $comma_values = explode( ',', $meta_value);366 foreach ( $comma_values as $value) {367 $value = trim( $value);368 if ( is_numeric($value) && $value > 0) {369 $used_image_ids[] = intval( $value);370 } 371 } 372 } 373 374 if ( preg_match_all('/i:(\d+);/', $meta_value, $matches)) {375 foreach ( $matches[1] as $id) {376 if ( $id > 0) {377 $used_image_ids[] = intval( $id);378 } 379 } 380 } 381 382 if ( preg_match_all('/"(\d+)"/', $meta_value, $matches)) {383 foreach ( $matches[1] as $id) {384 if ( $id > 0 && strlen($id) <= 10) {385 $used_image_ids[] = intval( $id);334 if ( strpos( $meta_value, ',' ) !== false ) { 335 $comma_values = explode( ',', $meta_value ); 336 foreach ( $comma_values as $value ) { 337 $value = trim( $value ); 338 if ( is_numeric( $value ) && $value > 0 ) { 339 $used_image_ids[] = intval( $value ); 340 } 341 } 342 } 343 344 if ( preg_match_all( '/i:(\d+);/', $meta_value, $matches ) ) { 345 foreach ( $matches[1] as $id ) { 346 if ( $id > 0 ) { 347 $used_image_ids[] = intval( $id ); 348 } 349 } 350 } 351 352 if ( preg_match_all( '/"(\d+)"/', $meta_value, $matches ) ) { 353 foreach ( $matches[1] as $id ) { 354 if ( $id > 0 && strlen( $id ) <= 10 ) { 355 $used_image_ids[] = intval( $id ); 386 356 } 387 357 } … … 405 375 "); 406 376 407 foreach ( $acf_specific_query as $acf_value) {408 if ( empty($acf_value)) {377 foreach ( $acf_specific_query as $acf_value ) { 378 if ( empty( $acf_value ) ) { 409 379 continue; 410 380 } 411 381 412 if ( is_numeric($acf_value) && $acf_value > 0) {413 $used_image_ids[] = intval( $acf_value);382 if ( is_numeric( $acf_value ) && $acf_value > 0 ) { 383 $used_image_ids[] = intval( $acf_value ); 414 384 } else { 415 if (is_serialized($acf_value)) { 416 $unserialized = @unserialize($acf_value); 417 if (is_array($unserialized)) { 418 array_walk_recursive($unserialized, function ($item) use (&$used_image_ids) { 419 if (is_numeric($item) && $item > 0 && $item < 999999999) { 420 $used_image_ids[] = intval($item); 385 if ( is_serialized( $acf_value ) ) { 386 $unserialized = @unserialize( $acf_value ); 387 if ( is_array( $unserialized ) ) { 388 array_walk_recursive( $unserialized, function( $item, $key ) use ( &$used_image_ids ) { 389 // Skip common non-ID numeric keys found in attachment metadata and other places 390 if ( in_array( $key, ['width', 'height', 'file_size', 'filesize', 'size', 'length', 'bitrate', 'duration', 'uploaded', 'year', 'month', 'day'], true ) ) { 391 return; 392 } 393 394 if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) { 395 $used_image_ids[] = intval( $item ); 421 396 } 422 397 }); … … 427 402 } 428 403 429 // OPTIMIZED: Increased batch size from 100 to 2000 430 // At 5-min intervals: 10M images / 100 = 33,333 runs = 115+ days 431 // With 2000: 10M / 2000 = 5,000 runs = ~17 days at 5-min or ~6 days at 2-min 432 // Memory usage is acceptable since we're just extracting IDs (not full objects) 433 $batch_size = 2000; 404 // Check Elementor Data 405 $elementor_meta_values = $this->get_cached_db_result(" 406 SELECT meta_value 407 FROM {$wpdb->postmeta} 408 WHERE meta_key = '_elementor_data' 409 AND meta_value != '' 410 "); 411 412 if ( $elementor_meta_values ) { 413 foreach ( $elementor_meta_values as $meta_value ) { 414 $data = json_decode( $meta_value, true ); 415 if ( is_array( $data ) ) { 416 array_walk_recursive( $data, function( $item, $key ) use ( &$used_image_ids ) { 417 if ( $key === 'id' && is_numeric( $item ) && $item > 0 ) { 418 $used_image_ids[] = intval( $item ); 419 } 420 }); 421 } 422 } 423 } 424 425 $batch_size = 100; 434 426 $offset = 0; 435 $max_execution_time = 25; // seconds - leave buffer for cron 436 $start_time = microtime(true); 437 438 while (true) { 427 428 while ( true ) { 439 429 $sql = $wpdb->prepare( 440 430 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", … … 443 433 $offset 444 434 ); 445 $posts_with_content = $this->get_cached_db_result( $sql, 'results');446 447 if ( empty($posts_with_content)) {435 $posts_with_content = $this->get_cached_db_result( $sql, 'results' ); 436 437 if ( empty( $posts_with_content ) ) { 448 438 break; 449 439 } 450 440 451 foreach ( $posts_with_content as $post) {452 preg_match_all( '/wp-image-(\d+)/', $post->post_content, $matches);453 if ( !empty($matches[1])) {454 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));441 foreach ( $posts_with_content as $post ) { 442 preg_match_all( '/wp-image-(\d+)/', $post->post_content, $matches ); 443 if ( ! empty( $matches[1] ) ) { 444 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 455 445 } 456 446 } … … 461 451 // Scan Gutenberg image blocks that may not include the wp-image- class 462 452 $offset_blocks = 0; 463 while ( true) {453 while ( true ) { 464 454 $sql_blocks = $wpdb->prepare( 465 455 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", … … 468 458 $offset_blocks 469 459 ); 470 $posts_with_blocks = $this->get_cached_db_result( $sql_blocks, 'results');471 472 if ( empty($posts_with_blocks)) {460 $posts_with_blocks = $this->get_cached_db_result( $sql_blocks, 'results' ); 461 462 if ( empty( $posts_with_blocks ) ) { 473 463 break; 474 464 } 475 465 476 foreach ( $posts_with_blocks as $post) {477 if ( preg_match_all('/<!--\s*wp:image\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches)) {478 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));466 foreach ( $posts_with_blocks as $post ) { 467 if ( preg_match_all( '/<!--\s*wp:image\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 468 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 479 469 } 480 470 } … … 485 475 // Scan other Gutenberg media blocks (Cover, Media & Text, Video, Audio, File) 486 476 $offset_blocks_ext = 0; 487 while ( true) {477 while ( true ) { 488 478 $sql_blocks_ext = $wpdb->prepare( 489 479 "SELECT ID, post_content FROM {$wpdb->posts} WHERE ( … … 502 492 $offset_blocks_ext 503 493 ); 504 $posts_with_blocks_ext = $this->get_cached_db_result( $sql_blocks_ext, 'results');505 506 if ( empty($posts_with_blocks_ext)) {494 $posts_with_blocks_ext = $this->get_cached_db_result( $sql_blocks_ext, 'results' ); 495 496 if ( empty( $posts_with_blocks_ext ) ) { 507 497 break; 508 498 } 509 499 510 foreach ( $posts_with_blocks_ext as $post) {500 foreach ( $posts_with_blocks_ext as $post ) { 511 501 // Cover block 512 if ( preg_match_all('/<!--\s*wp:cover\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches)) {513 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));502 if ( preg_match_all( '/<!--\s*wp:cover\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 503 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 514 504 } 515 505 // Media & Text block (uses mediaId) 516 if ( preg_match_all('/<!--\s*wp:media-text\s*{[^}]*\"mediaId\"\s*:\s*(\d+)/', $post->post_content, $matches)) {517 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));506 if ( preg_match_all( '/<!--\s*wp:media-text\s*{[^}]*\"mediaId\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 507 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 518 508 } 519 509 // Video block 520 if ( preg_match_all('/<!--\s*wp:video\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches)) {521 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));510 if ( preg_match_all( '/<!--\s*wp:video\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 511 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 522 512 } 523 513 // Audio block 524 if ( preg_match_all('/<!--\s*wp:audio\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches)) {525 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));514 if ( preg_match_all( '/<!--\s*wp:audio\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 515 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 526 516 } 527 517 // File block 528 if ( preg_match_all('/<!--\s*wp:file\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches)) {529 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));518 if ( preg_match_all( '/<!--\s*wp:file\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) { 519 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 530 520 } 531 521 } … … 536 526 // Scan gallery shortcodes and Gutenberg gallery blocks 537 527 $offset_gallery = 0; 538 while ( true) {528 while ( true ) { 539 529 $sql_gallery = $wpdb->prepare( 540 530 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d", … … 543 533 $offset_gallery 544 534 ); 545 $posts_with_gallery = $this->get_cached_db_result( $sql_gallery, 'results');546 547 if ( empty($posts_with_gallery)) {535 $posts_with_gallery = $this->get_cached_db_result( $sql_gallery, 'results' ); 536 537 if ( empty( $posts_with_gallery ) ) { 548 538 break; 549 539 } 550 540 551 foreach ( $posts_with_gallery as $post) {552 if ( preg_match_all('/\[gallery[^\]]*ids=\"([^\"]+)\"/', $post->post_content, $matches)) {553 foreach ( $matches[1] as $csv) {554 $ids = array_filter( array_map('intval', explode(',', $csv)));555 if ( $ids) {556 $used_image_ids = array_merge( $used_image_ids, $ids);557 } 558 } 559 } 560 if ( preg_match_all('/<!--\s*wp:gallery\s*{[^}]*\"ids\"\s*:\s*\[([^\]]+)\]/', $post->post_content, $matches2)) {561 foreach ( $matches2[1] as $list) {562 $ids = array_filter( array_map('intval', preg_split('/\s*,\s*/', preg_replace('/[^\d,]/', '', $list))));563 if ( $ids) {564 $used_image_ids = array_merge( $used_image_ids, $ids);541 foreach ( $posts_with_gallery as $post ) { 542 if ( preg_match_all( '/\[gallery[^\]]*ids=\"([^\"]+)\"/', $post->post_content, $matches ) ) { 543 foreach ( $matches[1] as $csv ) { 544 $ids = array_filter( array_map( 'intval', explode( ',', $csv ) ) ); 545 if ( $ids ) { 546 $used_image_ids = array_merge( $used_image_ids, $ids ); 547 } 548 } 549 } 550 if ( preg_match_all( '/<!--\s*wp:gallery\s*{[^}]*\"ids\"\s*:\s*\[([^\]]+)\]/', $post->post_content, $matches2 ) ) { 551 foreach ( $matches2[1] as $list ) { 552 $ids = array_filter( array_map( 'intval', preg_split( '/\s*,\s*/', preg_replace( '/[^\d,]/', '', $list ) ) ) ); 553 if ( $ids ) { 554 $used_image_ids = array_merge( $used_image_ids, $ids ); 565 555 } 566 556 } … … 573 563 // Elementor: scan all posts with _elementor_data in batches and extract image IDs more accurately 574 564 $el_offset = 0; 575 while ( true) {565 while ( true ) { 576 566 $sql_el = $wpdb->prepare( 577 567 "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_elementor_data' LIMIT %d OFFSET %d", … … 579 569 $el_offset 580 570 ); 581 $elementor_posts = $this->get_cached_db_result( $sql_el);582 583 if ( empty($elementor_posts)) {571 $elementor_posts = $this->get_cached_db_result( $sql_el ); 572 573 if ( empty( $elementor_posts ) ) { 584 574 break; 585 575 } 586 576 587 foreach ( $elementor_posts as $post_id) {588 $raw = get_post_meta( $post_id, '_elementor_data', true);589 if ( empty($raw) || !is_string($raw)) {577 foreach ( $elementor_posts as $post_id ) { 578 $raw = get_post_meta( $post_id, '_elementor_data', true ); 579 if ( empty( $raw ) ) { 590 580 continue; 591 581 } 592 593 // OPTIMIZED: Use regex-based extraction instead of json_decode 594 // This reduces memory from 2-5MB per page to ~100KB 595 // json_decode of 500KB creates huge PHP array structure 596 597 // Pattern 1: "id":123,"url": (image widget) 598 if (preg_match_all('/"id"\s*:\s*(\d+)\s*,\s*"url"\s*:/i', $raw, $matches)) { 599 foreach ($matches[1] as $id) { 600 $id = intval($id); 601 if ($id > 0 && $id < 999999999) { 602 $used_image_ids[] = $id; 603 } 604 } 605 } 606 607 // Pattern 2: "ids":[1,2,3] (gallery array) 608 if (preg_match_all('/"ids"\s*:\s*\[([^\]]+)\]/i', $raw, $matches)) { 609 foreach ($matches[1] as $csv) { 610 $gallery_ids = array_filter(array_map('intval', preg_split('/\s*,\s*/', $csv))); 611 foreach ($gallery_ids as $id) { 612 if ($id > 0 && $id < 999999999) { 613 $used_image_ids[] = $id; 582 $data = json_decode( $raw, true ); 583 if ( ! is_array( $data ) ) { 584 continue; 585 } 586 587 $extract_ids = function( $node ) use ( &$used_image_ids, &$extract_ids ) { 588 if ( is_array( $node ) ) { 589 // Direct image-like object: { id: 123, url: "..." } 590 if ( isset( $node['id'] ) && is_numeric( $node['id'] ) ) { 591 $has_url = isset( $node['url'] ) && is_string( $node['url'] ); 592 $looks_like_image = isset( $node['size'] ) || isset( $node['source'] ) || isset( $node['image'] ) || isset( $node['image_size'] ); 593 if ( $has_url || $looks_like_image ) { 594 $used_image_ids[] = intval( $node['id'] ); 614 595 } 615 596 } 616 } 617 } 618 619 // Pattern 3: "ids":"1,2,3" (gallery string) 620 if (preg_match_all('/"ids"\s*:\s*"([^"]+)"/i', $raw, $matches)) { 621 foreach ($matches[1] as $csv) { 622 $gallery_ids = array_filter(array_map('intval', explode(',', $csv))); 623 foreach ($gallery_ids as $id) { 624 if ($id > 0 && $id < 999999999) { 625 $used_image_ids[] = $id; 597 598 // Check for 'ids' key (arrays or comma-separated strings), common in galleries 599 if ( isset( $node['ids'] ) ) { 600 $ids_val = $node['ids']; 601 if ( is_array( $ids_val ) ) { 602 foreach ( $ids_val as $id ) { 603 if ( is_numeric( $id ) && $id > 0 ) { 604 $used_image_ids[] = intval( $id ); 605 } 606 } 607 } elseif ( is_string( $ids_val ) ) { 608 $ids = array_filter( array_map( 'intval', explode( ',', $ids_val ) ) ); 609 if ( ! empty( $ids ) ) { 610 $used_image_ids = array_merge( $used_image_ids, $ids ); 611 } 626 612 } 627 613 } 628 } 629 } 630 631 // Pattern 4: "image":{"id":123} (nested image) 632 if (preg_match_all('/"image"\s*:\s*\{[^}]*"id"\s*:\s*(\d+)/i', $raw, $matches)) { 633 foreach ($matches[1] as $id) { 634 $id = intval($id); 635 if ($id > 0 && $id < 999999999) { 636 $used_image_ids[] = $id; 637 } 638 } 639 } 640 641 // Pattern 5: wp-image-123 in HTML 642 if (preg_match_all('/wp-image-(\d+)/i', $raw, $matches)) { 643 foreach ($matches[1] as $id) { 644 $id = intval($id); 645 if ($id > 0 && $id < 999999999) { 646 $used_image_ids[] = $id; 647 } 648 } 649 } 614 615 foreach ( $node as $v ) { 616 $extract_ids( $v ); 617 } 618 } 619 }; 620 621 $extract_ids( $data ); 650 622 } 651 623 … … 655 627 // Divi Builder: scan posts using Divi shortcodes and extract image IDs/URLs 656 628 $divi_offset = 0; 657 while ( true) {629 while ( true ) { 658 630 $sql_divi = $wpdb->prepare( 659 631 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d", … … 662 634 $divi_offset 663 635 ); 664 $divi_posts = $this->get_cached_db_result( $sql_divi, 'results');665 666 if ( empty($divi_posts)) {636 $divi_posts = $this->get_cached_db_result( $sql_divi, 'results' ); 637 638 if ( empty( $divi_posts ) ) { 667 639 break; 668 640 } 669 641 670 foreach ( $divi_posts as $post) {642 foreach ( $divi_posts as $post ) { 671 643 $content = $post->post_content; 672 644 673 645 // IDs stored directly on modules (e.g., image_id, background_image_id) 674 if ( preg_match_all('/\\b(image_id|background_image_id|logo_image_id|featured_image_id)\\s*=\\s*\"(\\d+)\"/i', $content, $m_ids)) {675 foreach ( $m_ids[2] as $id) {676 $id = intval( $id);677 if ( $id > 0) {646 if ( preg_match_all( '/\\b(image_id|background_image_id|logo_image_id|featured_image_id)\\s*=\\s*\"(\\d+)\"/i', $content, $m_ids ) ) { 647 foreach ( $m_ids[2] as $id ) { 648 $id = intval( $id ); 649 if ( $id > 0 ) { 678 650 $used_image_ids[] = $id; 679 651 } … … 682 654 683 655 // Gallery IDs in Divi gallery module (ids or gallery_ids) 684 if ( preg_match_all('/\\b(ids|gallery_ids)\\s*=\\s*\"([\\d,\\s]+)\"/i', $content, $m_gal)) {685 foreach ( $m_gal[2] as $csv) {686 $ids = array_filter( array_map('intval', preg_split('/\\s*,\\s*/', $csv)));687 if ( $ids) {688 $used_image_ids = array_merge( $used_image_ids, $ids);656 if ( preg_match_all( '/\\b(ids|gallery_ids)\\s*=\\s*\"([\\d,\\s]+)\"/i', $content, $m_gal ) ) { 657 foreach ( $m_gal[2] as $csv ) { 658 $ids = array_filter( array_map( 'intval', preg_split( '/\\s*,\\s*/', $csv ) ) ); 659 if ( $ids ) { 660 $used_image_ids = array_merge( $used_image_ids, $ids ); 689 661 } 690 662 } … … 692 664 693 665 // URLs stored on modules (src, url, image_url, background_image) 694 if ( preg_match_all('/\\b(src|url|image_url|background_image)\\s*=\\s*\"([^\"]+)\"/i', $content, $m_urls)) {695 foreach ( $m_urls[2] as $url) {696 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url($url);697 if ( $id) {698 $used_image_ids[] = intval( $id);666 if ( preg_match_all( '/\\b(src|url|image_url|background_image)\\s*=\\s*\"([^\"]+)\"/i', $content, $m_urls ) ) { 667 foreach ( $m_urls[2] as $url ) { 668 $id = attachment_url_to_postid( $url ); 669 if ( $id ) { 670 $used_image_ids[] = intval( $id ); 699 671 } 700 672 } … … 707 679 // Generic: scan direct uploads URLs in post content and map to attachment IDs 708 680 $offset_urls = 0; 709 while ( true) {681 while ( true ) { 710 682 $sql_urls = $wpdb->prepare( 711 683 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d", … … 714 686 $offset_urls 715 687 ); 716 $posts_with_urls = $this->get_cached_db_result( $sql_urls, 'results');717 718 if ( empty($posts_with_urls)) {688 $posts_with_urls = $this->get_cached_db_result( $sql_urls, 'results' ); 689 690 if ( empty( $posts_with_urls ) ) { 719 691 break; 720 692 } 721 693 722 foreach ( $posts_with_urls as $post) {694 foreach ( $posts_with_urls as $post ) { 723 695 // Use double-quoted PHP string to avoid premature termination when matching single quotes inside the pattern 724 if ( preg_match_all("/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $post->post_content, $m_url_all)) {725 foreach ( $m_url_all[0] as $url) {726 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url($url);727 if ( $id) {728 $used_image_ids[] = intval( $id);696 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $post->post_content, $m_url_all ) ) { 697 foreach ( $m_url_all[0] as $url ) { 698 $id = attachment_url_to_postid( $url ); 699 if ( $id ) { 700 $used_image_ids[] = intval( $id ); 729 701 } 730 702 } … … 737 709 // Scan post_excerpt (e.g. WooCommerce Short Description) 738 710 $offset_excerpt = 0; 739 while ( true) {711 while ( true ) { 740 712 $sql_excerpt = $wpdb->prepare( 741 713 "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", … … 745 717 $offset_excerpt 746 718 ); 747 $posts_with_excerpt = $this->get_cached_db_result( $sql_excerpt, 'results');748 749 if ( empty($posts_with_excerpt)) {719 $posts_with_excerpt = $this->get_cached_db_result( $sql_excerpt, 'results' ); 720 721 if ( empty( $posts_with_excerpt ) ) { 750 722 break; 751 723 } 752 724 753 foreach ( $posts_with_excerpt as $post) {725 foreach ( $posts_with_excerpt as $post ) { 754 726 // Check for wp-image- ID 755 if ( preg_match_all('/wp-image-(\d+)/', $post->post_excerpt, $matches)) {756 if ( !empty($matches[1])) {757 $used_image_ids = array_merge( $used_image_ids, array_map('intval', $matches[1]));727 if ( preg_match_all( '/wp-image-(\d+)/', $post->post_excerpt, $matches ) ) { 728 if ( ! empty( $matches[1] ) ) { 729 $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) ); 758 730 } 759 731 } 760 732 761 733 // Check for direct URLs 762 if ( preg_match_all("/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $post->post_excerpt, $m_url_all)) {763 foreach ( $m_url_all[0] as $url) {764 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url($url);765 if ( $id) {766 $used_image_ids[] = intval( $id);734 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $post->post_excerpt, $m_url_all ) ) { 735 foreach ( $m_url_all[0] as $url ) { 736 $id = attachment_url_to_postid( $url ); 737 if ( $id ) { 738 $used_image_ids[] = intval( $id ); 767 739 } 768 740 } … … 775 747 // Scan postmeta for raw URLs (custom fields, metaboxes) 776 748 $offset_meta = 0; 777 while ( true) {749 while ( true ) { 778 750 $sql_meta = $wpdb->prepare( 779 751 "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_value LIKE %s LIMIT %d OFFSET %d", … … 782 754 $offset_meta 783 755 ); 784 $meta_values = $this->get_cached_db_result( $sql_meta, 'col');785 786 if ( empty($meta_values)) {756 $meta_values = $this->get_cached_db_result( $sql_meta, 'col' ); 757 758 if ( empty( $meta_values ) ) { 787 759 break; 788 760 } 789 761 790 foreach ( $meta_values as $val) {762 foreach ( $meta_values as $val ) { 791 763 // Check for direct URLs 792 if ( preg_match_all("/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all)) {793 foreach ( $m_url_all[0] as $url) {764 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all ) ) { 765 foreach ( $m_url_all[0] as $url ) { 794 766 // Clean up URL (remove query strings, etc if needed, though attachment_url_to_postid handles some) 795 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url($url);796 if ( $id) {797 $used_image_ids[] = intval( $id);767 $id = attachment_url_to_postid( $url ); 768 if ( $id ) { 769 $used_image_ids[] = intval( $id ); 798 770 } 799 771 } … … 805 777 // Scan options for raw URLs (theme settings, custom options) 806 778 $offset_options = 0; 807 while ( true) {779 while ( true ) { 808 780 $sql_options = $wpdb->prepare( 809 781 "SELECT option_value FROM {$wpdb->options} WHERE option_value LIKE %s LIMIT %d OFFSET %d", … … 812 784 $offset_options 813 785 ); 814 $option_values = $this->get_cached_db_result( $sql_options, 'col');815 816 if ( empty($option_values)) {786 $option_values = $this->get_cached_db_result( $sql_options, 'col' ); 787 788 if ( empty( $option_values ) ) { 817 789 break; 818 790 } 819 791 820 foreach ( $option_values as $val) {792 foreach ( $option_values as $val ) { 821 793 // Check for direct URLs 822 if ( preg_match_all("/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all)) {823 foreach ( $m_url_all[0] as $url) {824 $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url($url);825 if ( $id) {826 $used_image_ids[] = intval( $id);794 if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all ) ) { 795 foreach ( $m_url_all[0] as $url ) { 796 $id = attachment_url_to_postid( $url ); 797 if ( $id ) { 798 $used_image_ids[] = intval( $id ); 827 799 } 828 800 } … … 832 804 } 833 805 834 $site_icon_id = get_option( 'site_icon');835 if ( $site_icon_id) {836 $used_image_ids[] = intval( $site_icon_id);837 } 838 839 $custom_logo_id = get_theme_mod( 'custom_logo');840 if ( $custom_logo_id) {841 $used_image_ids[] = intval( $custom_logo_id);842 } 843 844 $header_image_id = get_theme_mod( 'header_image_data');845 if ( is_array($header_image_id) && isset($header_image_id['attachment_id'])) {846 $used_image_ids[] = intval( $header_image_id['attachment_id']);847 } 848 849 $background_image_id = get_theme_mod( 'background_image_thumb');850 if ( $background_image_id) {851 $used_image_ids[] = intval( $background_image_id);852 } 853 854 $used_image_ids = array_unique( array_filter(array_map('intval', $used_image_ids), function ($id) {806 $site_icon_id = get_option( 'site_icon' ); 807 if ( $site_icon_id ) { 808 $used_image_ids[] = intval( $site_icon_id ); 809 } 810 811 $custom_logo_id = get_theme_mod( 'custom_logo' ); 812 if ( $custom_logo_id ) { 813 $used_image_ids[] = intval( $custom_logo_id ); 814 } 815 816 $header_image_id = get_theme_mod( 'header_image_data' ); 817 if ( is_array( $header_image_id ) && isset( $header_image_id['attachment_id'] ) ) { 818 $used_image_ids[] = intval( $header_image_id['attachment_id'] ); 819 } 820 821 $background_image_id = get_theme_mod( 'background_image_thumb' ); 822 if ( $background_image_id ) { 823 $used_image_ids[] = intval( $background_image_id ); 824 } 825 826 $used_image_ids = array_unique( array_filter( array_map( 'intval', $used_image_ids ), function( $id ) { 855 827 return $id > 0 && $id < 999999999; 856 828 })); … … 859 831 } 860 832 861 public function scan_and_save_snapshot() 862 { 833 public function scan_and_save_snapshot() { 863 834 global $wpdb; 864 835 865 $table_name = $wpdb->prefix . 'media_tracker_index'; 866 867 // Use SQL LEFT JOIN to find unused IDs directly in database 868 // This avoids loading millions of IDs into PHP memory 869 // Much more efficient than array_diff() for large datasets 870 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 871 $unused_ids = $wpdb->get_col( 872 "SELECT p.ID 873 FROM {$wpdb->posts} p 874 LEFT JOIN {$table_name} i ON p.ID = i.media_id 875 WHERE p.post_type = 'attachment' 876 AND p.post_status = 'inherit' 877 AND i.media_id IS NULL 878 ORDER BY p.ID ASC" 879 ); 880 881 // Sanitize (minimal memory usage - just type conversion) 882 $unused_ids = array_map('intval', $unused_ids); 883 884 // Calculate size using cached values with timeout handling 885 $total_size = \Media_Tracker\File_Size_Cache::calculate_total_size_progressive($unused_ids); 836 // Force calculation of used IDs 837 $used_image_ids = $this->get_used_media_ids(); 838 839 // Get all attachment IDs 840 $all_attachments = $this->get_cached_db_result( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit'" ); 841 842 // Calculate unused IDs 843 $unused_ids = array_diff( $all_attachments, $used_image_ids ); 844 845 // Filter and sanitize 846 $unused_ids = array_unique( array_filter( array_map( 'intval', $unused_ids ) ) ); 847 848 // Calculate size 849 $total_size = 0; 850 foreach ( $unused_ids as $id ) { 851 $file_path = get_attached_file( $id ); 852 if ( $file_path && file_exists( $file_path ) ) { 853 $total_size += filesize( $file_path ); 854 } 855 } 886 856 887 857 // Save snapshot and stats 888 update_option('media_tracker_unused_ids_snapshot', $unused_ids, false); 889 update_option('unused_media_last_cache_time', time()); 890 update_option('media_tracker_unused_count_last_scan', count($unused_ids)); 891 update_option('media_tracker_unused_size_last_scan', $total_size); 892 893 // PERFORMANCE: Clear transient cache so overview tab shows fresh count immediately 894 delete_transient('media_tracker_unused_count_cache'); 895 896 // Clear result cache to force fresh data on next page load 897 $this->force_clear_cache(); 858 update_option( 'media_tracker_unused_ids_snapshot', $unused_ids, false ); 859 update_option( 'unused_media_last_cache_time', time() ); 860 update_option( 'media_tracker_unused_count_last_scan', count( $unused_ids ) ); 861 update_option( 'media_tracker_unused_size_last_scan', $total_size ); 862 863 // Invalidate dashboard stats cache to ensure overview tab is updated 864 delete_transient( 'media_tracker_dashboard_stats_v8' ); 898 865 899 866 return count($unused_ids); 900 867 } 901 868 902 public function prepare_items() 903 { 869 public function prepare_items() { 904 870 global $wpdb; 905 871 … … 907 873 908 874 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Search request is a GET request and safe. 909 $search = isset( $_REQUEST['s']) ? sanitize_text_field(wp_unslash($_REQUEST['s'])) : '';910 911 $per_page = $this->get_items_per_page('unused_media_cleaner_per_page', 10);875 $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; 876 877 $per_page = $this->get_items_per_page( 'unused_media_cleaner_per_page', 10 ); 912 878 $current_page = $this->get_pagenum(); 913 $offset = ($current_page - 1) * $per_page; 914 915 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Orderby/order are GET parameters and safe. 916 $orderby_param = isset($_REQUEST['orderby']) ? sanitize_text_field(wp_unslash($_REQUEST['orderby'])) : 'date'; 917 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Orderby/order are GET parameters and safe. 918 $order_param = isset($_REQUEST['order']) ? sanitize_text_field(wp_unslash($_REQUEST['order'])) : 'DESC'; 919 920 // PERFORMANCE OPTIMIZATION: Use direct SQL queries instead of loading full snapshot 921 // This reduces memory usage from 8-10MB to <1MB and improves load time by 80-90% 922 $table_name = $wpdb->prefix . 'media_tracker_index'; 923 924 // Build WHERE conditions 879 $offset = ( $current_page - 1 ) * $per_page; 880 881 // Retrieve from snapshot 882 $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() ); 883 884 // Handle search if needed (filter snapshot IDs by search term) 885 // This requires a query if search is present, but restricted to snapshot IDs. 886 925 887 $where_conditions = [ 926 888 "p.post_type = 'attachment'", 927 "p.post_status = 'inherit'", 928 "i.media_id IS NULL" 889 "p.post_status = 'inherit'" 929 890 ]; 930 891 931 if ($this->author_id) { 932 $where_conditions[] = $wpdb->prepare('p.post_author = %d', $this->author_id); 933 } 934 935 if ($search) { 936 $where_conditions[] = $wpdb->prepare('p.post_title LIKE %s', '%' . $wpdb->esc_like($search) . '%'); 937 } 938 939 $where_clause = 'WHERE ' . implode(' AND ', $where_conditions); 940 941 // Map orderby parameter to actual column 942 $orderby_map = array( 943 'title' => 'p.post_title', 944 'post_title' => 'p.post_title', 945 'author' => 'p.post_author', 946 'post_author' => 'p.post_author', 947 'date' => 'p.post_date', 948 'post_date' => 'p.post_date', 949 ); 950 951 $orderby_column = isset($orderby_map[$orderby_param]) ? $orderby_map[$orderby_param] : 'p.post_date'; 952 $order_direction = ('ASC' === strtoupper($order_param)) ? 'ASC' : 'DESC'; 953 954 // Get total count using SQL COUNT (much faster than loading all IDs) 955 $count_query = " 956 SELECT COUNT(*) 957 FROM {$wpdb->posts} p 958 LEFT JOIN {$table_name} i ON p.ID = i.media_id 959 $where_clause 960 "; 961 962 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 963 $total_items = (int) $wpdb->get_var($count_query); 964 965 // Get paginated items using direct SQL query 966 $items_query = " 967 SELECT p.* 968 FROM {$wpdb->posts} p 969 LEFT JOIN {$table_name} i ON p.ID = i.media_id 970 $where_clause 971 ORDER BY $orderby_column $order_direction 972 LIMIT %d OFFSET %d 973 "; 974 975 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 976 $this->items = $wpdb->get_results($wpdb->prepare($items_query, $per_page, $offset)); 977 978 $columns = $this->get_columns(); 979 $hidden = []; 980 $sortable = $this->get_sortable_columns(); 981 $this->_column_headers = [$columns, $hidden, $sortable]; 982 983 $this->set_pagination_args(array( 892 if ( empty( $unused_image_ids ) ) { 893 // No unused media found in snapshot (or not scanned yet) 894 $this->items = array(); 895 $total_items = 0; 896 } else { 897 $args = array( 898 'post_type' => 'attachment', 899 'post_status' => 'inherit', 900 'post__in' => $unused_image_ids, 901 'posts_per_page' => $per_page, 902 'paged' => $current_page, 903 'orderby' => $orderby_param, 904 'order' => $order_param, 905 ); 906 907 if ( $this->author_id ) { 908 $args['author'] = $this->author_id; 909 } 910 911 if ( $search ) { 912 $args['s'] = $search; 913 } 914 915 $query = new \WP_Query( $args ); 916 $this->items = $query->posts; 917 $total_items = $query->found_posts; 918 } 919 920 $columns = $this->get_columns(); 921 $hidden = []; 922 $sortable = $this->get_sortable_columns(); 923 $this->_column_headers = [ $columns, $hidden, $sortable ]; 924 925 $this->set_pagination_args( array( 984 926 'total_items' => $total_items, 985 'per_page' => $per_page, 986 'total_pages' => $per_page > 0 ? ceil($total_items / $per_page) : 0, 987 )); 988 } 989 990 private function should_invalidate_cache() 991 { 927 'per_page' => $per_page, 928 'total_pages' => $per_page > 0 ? ceil( $total_items / $per_page ) : 0, 929 ) ); 930 } 931 932 private function should_invalidate_cache() { 992 933 // Manual mode: only refresh cache when a scan is run. 993 934 // If enabled (default true), do NOT auto invalidate based on site activity. 994 $manual_mode = get_option( 'media_tracker_manual_scan', true);995 if ( $manual_mode) {935 $manual_mode = get_option( 'media_tracker_manual_scan', true ); 936 if ( $manual_mode ) { 996 937 return false; 997 938 } … … 999 940 global $wpdb; 1000 941 1001 $last_cache_time = get_option( 'unused_media_last_cache_time', 0);1002 1003 $recent_media_activity = $this->get_cached_db_result( $wpdb->prepare(942 $last_cache_time = get_option( 'unused_media_last_cache_time', 0 ); 943 944 $recent_media_activity = $this->get_cached_db_result( $wpdb->prepare( 1004 945 "SELECT COUNT(*)\n FROM {$wpdb->posts}\n WHERE post_type = 'attachment'\n AND post_modified_gmt > %s", 1005 gmdate( 'Y-m-d H:i:s', $last_cache_time)1006 ), 'var' );1007 1008 $recent_post_activity = $this->get_cached_db_result( $wpdb->prepare(946 gmdate( 'Y-m-d H:i:s', $last_cache_time ) 947 ), 'var' ); 948 949 $recent_post_activity = $this->get_cached_db_result( $wpdb->prepare( 1009 950 "SELECT COUNT(*)\n FROM {$wpdb->posts}\n WHERE post_type IN ('post', 'page', 'product')\n AND post_modified_gmt > %s", 1010 gmdate('Y-m-d H:i:s', $last_cache_time) 1011 ), 'var'); 1012 1013 return ($recent_media_activity > 0 || $recent_post_activity > 0); 1014 } 1015 1016 public function force_clear_cache() 1017 { 951 gmdate( 'Y-m-d H:i:s', $last_cache_time ) 952 ), 'var' ); 953 954 return ( $recent_media_activity > 0 || $recent_post_activity > 0 ); 955 } 956 957 public function force_clear_cache() { 1018 958 $this->clear_cache(); 1019 delete_option('unused_media_last_cache_time'); 1020 } 1021 1022 public function get_total_items($search = '', $force_fresh = false) 1023 { 959 delete_option( 'unused_media_last_cache_time' ); 960 } 961 962 public function get_total_items( $search = '', $force_fresh = false ) { 1024 963 global $wpdb; 1025 964 1026 // PERFORMANCE OPTIMIZATION: Use SQL COUNT instead of loading full snapshot 1027 $table_name = $wpdb->prefix . 'media_tracker_index'; 965 // Retrieve from snapshot 966 $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() ); 967 968 if ( empty( $unused_image_ids ) ) { 969 return 0; 970 } 1028 971 1029 972 $where_conditions = [ 1030 973 "p.post_type = 'attachment'", 1031 "p.post_status = 'inherit'", 1032 "i.media_id IS NULL" 974 "p.post_status = 'inherit'" 1033 975 ]; 1034 976 1035 if ($this->author_id) { 1036 $where_conditions[] = $wpdb->prepare('p.post_author = %d', $this->author_id); 1037 } 1038 1039 if ($search) { 1040 $where_conditions[] = $wpdb->prepare('p.post_title LIKE %s', '%' . $wpdb->esc_like($search) . '%'); 1041 } 1042 1043 $where_clause = 'WHERE ' . implode(' AND ', $where_conditions); 977 $ids_placeholder = implode( ',', array_map( 'intval', $unused_image_ids ) ); 978 $where_conditions[] = "p.ID IN ($ids_placeholder)"; 979 980 if ( $this->author_id ) { 981 $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id ); 982 } 983 984 if ( $search ) { 985 $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' ); 986 } 987 988 $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); 1044 989 1045 990 $query = " 1046 991 SELECT COUNT(*) 1047 992 FROM {$wpdb->posts} p 1048 LEFT JOIN {$table_name} i ON p.ID = i.media_id1049 993 $where_clause 1050 994 "; 1051 995 1052 return $this->get_cached_db_result($query, 'var'); 1053 } 1054 1055 public function get_fresh_total_items($search = '') 1056 { 1057 return $this->get_total_items($search, true); 996 return $this->get_cached_db_result( $query, 'var' ); 997 } 998 999 public function get_fresh_total_items( $search = '' ) { 1000 return $this->get_total_items( $search, true ); 1058 1001 } 1059 1002 … … 1064 1007 * @return array Array of unused media IDs. 1065 1008 */ 1066 public function get_unused_media_ids($search = '') 1067 { 1009 public function get_unused_media_ids( $search = '' ) { 1068 1010 global $wpdb; 1069 1011 1070 // PERFORMANCE OPTIMIZATION: Use SQL query instead of loading snapshot 1071 $table_name = $wpdb->prefix . 'media_tracker_index'; 1012 // Retrieve from snapshot 1013 $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() ); 1014 1015 if ( empty( $unused_image_ids ) ) { 1016 return array(); 1017 } 1072 1018 1073 1019 // Build query to get all unused media 1074 1020 $where_conditions = [ 1075 1021 "p.post_type = 'attachment'", 1076 "p.post_status = 'inherit'", 1077 "i.media_id IS NULL" 1022 "p.post_status = 'inherit'" 1078 1023 ]; 1079 1024 1080 if ($this->author_id) { 1081 $where_conditions[] = $wpdb->prepare('p.post_author = %d', $this->author_id); 1082 } 1083 1084 if ($search) { 1085 $where_conditions[] = $wpdb->prepare('p.post_title LIKE %s', '%' . $wpdb->esc_like($search) . '%'); 1086 } 1087 1088 $where_clause = 'WHERE ' . implode(' AND ', $where_conditions); 1025 $ids_placeholder = implode( ',', array_map( 'intval', $unused_image_ids ) ); 1026 $where_conditions[] = "p.ID IN ($ids_placeholder)"; 1027 1028 if ( $this->author_id ) { 1029 $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id ); 1030 } 1031 1032 if ( $search ) { 1033 $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' ); 1034 } 1035 1036 $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions ); 1089 1037 1090 1038 $query = " 1091 1039 SELECT p.ID 1092 1040 FROM {$wpdb->posts} p 1093 LEFT JOIN {$table_name} i ON p.ID = i.media_id1094 1041 $where_clause 1095 1042 "; 1096 1043 1097 return $this->get_cached_db_result($query); 1098 } 1099 1100 protected function column_cb($item) 1101 { 1044 return $this->get_cached_db_result( $query ); 1045 } 1046 1047 protected function column_cb( $item ) { 1102 1048 return sprintf( 1103 '<input type="checkbox" name="media[]" value="%s" />', 1104 $item->ID 1049 '<input type="checkbox" name="media[]" value="%s" />', $item->ID 1105 1050 ); 1106 1051 } 1107 1052 1108 protected function get_bulk_actions() 1109 { 1053 protected function get_bulk_actions() { 1110 1054 return [ 1111 'delete' => __( 'Delete permanently', 'media-tracker'),1055 'delete' => __( 'Delete permanently', 'media-tracker' ), 1112 1056 ]; 1113 1057 } 1114 1058 1115 protected function process_bulk_action() 1116 { 1117 if ('delete' === $this->current_action()) { 1059 protected function process_bulk_action() { 1060 if ( 'delete' === $this->current_action() ) { 1118 1061 // Verify nonce for bulk actions 1119 check_admin_referer( 'bulk-media');1120 1121 $media_ids = isset( $_REQUEST['media']) ? array_map('absint', (array) $_REQUEST['media']) : [];1122 1123 if ( !empty($media_ids)) {1124 foreach ( $media_ids as $media_id) {1062 check_admin_referer( 'bulk-media' ); 1063 1064 $media_ids = isset( $_REQUEST['media'] ) ? array_map( 'absint', (array) $_REQUEST['media'] ) : []; 1065 1066 if ( ! empty( $media_ids ) ) { 1067 foreach ( $media_ids as $media_id ) { 1125 1068 // Capability check per attachment 1126 if ( current_user_can('delete_post', $media_id)) {1127 wp_delete_attachment( $media_id, true);1069 if ( current_user_can( 'delete_post', $media_id ) ) { 1070 wp_delete_attachment( $media_id, true ); 1128 1071 } 1129 1072 } … … 1132 1075 $this->clear_cache(); 1133 1076 1134 $deleted_count = count( $media_ids);1077 $deleted_count = count( $media_ids ); 1135 1078 1136 1079 /* translators: %d: number of deleted media files */ 1137 set_transient('unused_media_delete_message', sprintf(__('%d media file(s) deleted successfully.', 'media-tracker'), $deleted_count), 30); 1138 } 1139 } 1140 } 1141 1142 private function clear_cache() 1143 { 1080 set_transient( 'unused_media_delete_message', sprintf( __( '%d media file(s) deleted successfully.', 'media-tracker' ), $deleted_count ), 30 ); 1081 } 1082 } 1083 } 1084 1085 private function clear_cache() { 1144 1086 global $wpdb; 1145 1087 1146 1088 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 1147 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_unused_media_%'");1089 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_unused_media_%'" ); 1148 1090 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 1149 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_unused_media_%'");1150 1151 delete_option( 'unused_media_last_cache_time');1091 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_unused_media_%'" ); 1092 1093 delete_option( 'unused_media_last_cache_time' ); 1152 1094 } 1153 1095 … … 1159 1101 * @return mixed Query result. 1160 1102 */ 1161 private function get_cached_db_result($query, $type = 'col') 1162 { 1103 private function get_cached_db_result( $query, $type = 'col' ) { 1163 1104 global $wpdb; 1164 1105 1165 $key = 'mt_db_' . md5( $query);1106 $key = 'mt_db_' . md5( $query ); 1166 1107 $group = 'media_tracker'; 1167 $result = wp_cache_get( $key, $group);1168 1169 if ( false === $result) {1170 if ( 'col' === $type) {1171 $result = $wpdb->get_col( $query); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared1172 } elseif ( 'results' === $type) {1173 $result = $wpdb->get_results( $query); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared1174 } elseif ( 'var' === $type) {1175 $result = $wpdb->get_var( $query); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared1176 } 1177 wp_cache_set( $key, $result, $group, 300);1108 $result = wp_cache_get( $key, $group ); 1109 1110 if ( false === $result ) { 1111 if ( 'col' === $type ) { 1112 $result = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared 1113 } elseif ( 'results' === $type ) { 1114 $result = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared 1115 } elseif ( 'var' === $type ) { 1116 $result = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared 1117 } 1118 wp_cache_set( $key, $result, $group, 300 ); 1178 1119 } 1179 1120 return $result; 1180 1121 } 1181 1122 1182 public function display_delete_message() 1183 { 1184 if ($message = get_transient('unused_media_delete_message')) { 1185 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($message) . '</p></div>'; 1186 delete_transient('unused_media_delete_message'); 1187 } 1188 } 1189 1190 public function display() 1191 { 1192 wp_nonce_field('bulk-media'); 1123 public function display_delete_message() { 1124 if ( $message = get_transient( 'unused_media_delete_message' ) ) { 1125 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>'; 1126 delete_transient( 'unused_media_delete_message' ); 1127 } 1128 } 1129 1130 public function display() { 1131 wp_nonce_field( 'bulk-media' ); 1193 1132 parent::display(); 1194 1133 } -
media-tracker/trunk/includes/Admin/views/media-tracker.php
r3454648 r3457950 21 21 <?php 22 22 $media_tracker_current_tab = media_tracker_get_current_tab(); 23 $media_tracker_tabs = array(24 'overview',25 'unused-media',26 'duplicates',27 'external-storage',28 'optimization',29 'duplicates-static',30 'security',31 'multisite',32 'settings',33 'documents',34 );35 23 36 if ( media_tracker_is_pro_active() ) { 37 $media_tracker_tabs[] = 'license'; 24 // Only load the current tab 25 $media_tracker_active_class = ' active'; 26 27 echo '<div id="tab-' . esc_attr( $media_tracker_current_tab ) . '" class="tab-content' . esc_attr( $media_tracker_active_class ) . '">'; 28 29 // Use template file based on tab name 30 if ( 'duplicates-static' === $media_tracker_current_tab ) { 31 media_tracker_get_template( 'tabs/tab-duplicates.php' ); 32 } else { 33 media_tracker_get_template( 'tabs/tab-' . $media_tracker_current_tab . '.php' ); 38 34 } 39 35 40 foreach ( $media_tracker_tabs as $media_tracker_tab ) { 41 $media_tracker_active_class = ( $media_tracker_tab === $media_tracker_current_tab ) ? ' active' : ''; 42 echo '<div id="tab-' . esc_attr( $media_tracker_tab ) . '" class="tab-content' . esc_attr( $media_tracker_active_class ) . '">'; 43 44 // Use template file based on tab name 45 if ( 'duplicates-static' === $media_tracker_tab ) { 46 media_tracker_get_template( 'tabs/tab-duplicates.php' ); 47 } else { 48 media_tracker_get_template( 'tabs/tab-' . $media_tracker_tab . '.php' ); 49 } 50 51 echo '</div>'; 52 } 36 echo '</div>'; 53 37 ?> 54 38 -
media-tracker/trunk/includes/Admin/views/tabs/tab-duplicates.php
r3454648 r3457950 41 41 $media_tracker_total_duplicate_images = count( $media_tracker_duplicate_ids_set ); 42 42 43 // Update stored count for dashboard overview and clear cache to reflect changes immediately. 44 if ( $media_tracker_total_duplicate_images !== (int) get_option( 'media_tracker_duplicate_count_last_scan' ) ) { 45 update_option( 'media_tracker_duplicate_count_last_scan', $media_tracker_total_duplicate_images ); 46 delete_transient( 'media_tracker_dashboard_stats_v8' ); 47 } 48 43 49 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verifying nonce is not required for reading sorting parameters from URL. 44 50 $media_tracker_current_sort = isset( $_GET['mt_dup_sort'] ) ? sanitize_key( wp_unslash( $_GET['mt_dup_sort'] ) ) : ''; … … 114 120 echo '</div>'; 115 121 echo '</div>'; 116 117 118 122 119 123 echo '<form method="post" id="mt-duplicate-form">'; … … 194 198 echo '<td>' . ( $media_tracker_size_display ? esc_html( $media_tracker_size_display ) : '—' ) . '</td>'; 195 199 echo '<td class="mt-text-center"><a href="' . esc_url( $media_tracker_edit_link ) . '" class="mt-link-clean">' . esc_html( $media_tracker_usage_count ) . '</a></td>'; 196 echo '<td><button type="button" class="button mt-dup-delete-single" data-id="' . esc_attr( $media_tracker_id ) . '"> ' . esc_html__( 'Delete', 'media-tracker' ) . '</button></td>';200 echo '<td><button type="button" class="button mt-dup-delete-single" data-id="' . esc_attr( $media_tracker_id ) . '"><span class="dashicons dashicons-trash"></span></button></td>'; 197 201 echo '</tr>'; 198 202 } -
media-tracker/trunk/includes/Admin/views/tabs/tab-overview.php
r3455634 r3457950 12 12 */ 13 13 14 defined( 'ABSPATH') || exit;14 defined( 'ABSPATH' ) || exit; 15 15 16 16 // Get real data for dashboard. 17 17 18 // Get unused media count and size from optimized SQL queries 19 // PERFORMANCE OPTIMIZATION: Use SQL COUNT with transient caching 20 global $wpdb; 21 $table_name = $wpdb->prefix . 'media_tracker_index'; 22 23 // Try to get count from cache first (5 minute cache) 24 $media_tracker_unused_count = get_transient('media_tracker_unused_count_cache'); 25 26 if (false === $media_tracker_unused_count) { 27 // Cache miss - run the query 28 $media_tracker_unused_count_query = " 29 SELECT COUNT(*) 30 FROM {$wpdb->posts} p 31 LEFT JOIN {$table_name} i ON p.ID = i.media_id 32 WHERE p.post_type = 'attachment' 33 AND p.post_status = 'inherit' 34 AND i.media_id IS NULL 35 "; 36 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 37 $media_tracker_unused_count = (int) $wpdb->get_var($media_tracker_unused_count_query); 38 39 // Cache for 5 minutes (300 seconds) 40 set_transient('media_tracker_unused_count_cache', $media_tracker_unused_count, 300); 18 // Get unused media count and size. 19 $media_tracker_unused_count = 0; 20 $media_tracker_unused_size = 0; 21 22 $media_tracker_has_scanned = get_option( 'media_tracker_duplicates_scanned' ); 23 24 if ( ! $media_tracker_has_scanned ) { 25 // Fallback: Check if any hashes exist in DB (migration support). 26 $media_tracker_hash_exists = false; 27 if ( class_exists( '\Media_Tracker\Admin\Duplicate_Images' ) ) { 28 $media_tracker_hash_exists = \Media_Tracker\Admin\Duplicate_Images::has_generated_hashes(); 29 } 30 if ( $media_tracker_hash_exists ) { 31 update_option( 'media_tracker_duplicates_scanned', true ); 32 $media_tracker_has_scanned = true; 33 delete_transient( 'media_tracker_dashboard_stats_v8' ); 34 } 41 35 } 42 36 43 // Get size from cached value (calculated during scan) 44 $media_tracker_unused_size = get_option('media_tracker_unused_size_last_scan', 0); 45 46 $media_tracker_has_scanned = get_option('media_tracker_duplicates_scanned'); 47 48 // Stats summary is still kept for other data (recent unused, etc) 49 $media_tracker_stats = get_option('media_tracker_stats_summary'); 50 51 // Get duplicate count from the SAME source as Duplicates tab (live count for accuracy) 52 $media_tracker_duplicate_count = 0; 53 if ($media_tracker_has_scanned && class_exists('\Media_Tracker\Admin\Duplicate_Images')) { 54 $media_tracker_duplicate_count = \Media_Tracker\Admin\Duplicate_Images::count_duplicate_attachments(); 37 // Check cached data first. 38 $media_tracker_cache_key = 'media_tracker_dashboard_stats_v8'; 39 $media_tracker_stats = get_transient( $media_tracker_cache_key ); 40 41 if ( false === $media_tracker_stats ) { 42 // Fast fallback: Use stored options from last scan to avoid heavy calculation on page load. 43 $media_tracker_unused_count = (int) get_option( 'media_tracker_unused_count_last_scan', 0 ); 44 $media_tracker_unused_size = (int) get_option( 'media_tracker_unused_size_last_scan', 0 ); 45 $media_tracker_duplicate_count = (int) get_option( 'media_tracker_duplicate_count_last_scan', 0 ); 46 47 $media_tracker_stats = array( 48 'unused_count' => $media_tracker_unused_count, 49 'unused_size' => $media_tracker_unused_size, 50 'duplicate_count' => $media_tracker_duplicate_count, 51 ); 52 set_transient( $media_tracker_cache_key, $media_tracker_stats, 30 * MINUTE_IN_SECONDS ); 53 } else { 54 $media_tracker_unused_count = $media_tracker_stats['unused_count']; 55 $media_tracker_unused_size = $media_tracker_stats['unused_size']; 56 $media_tracker_duplicate_count = $media_tracker_stats['duplicate_count']; 55 57 } 56 58 57 // Get total attachments from live count for accuracy58 $media_tracker_count_posts = wp_count_posts('attachment');59 // Get total attachments directly (real-time). 60 $media_tracker_count_posts = wp_count_posts( 'attachment' ); 59 61 $media_tracker_total_attachments = $media_tracker_count_posts->inherit; 60 61 $media_tracker_unused_size_formatted = size_format($media_tracker_unused_size); 62 $media_tracker_unused_size_formatted = size_format( $media_tracker_unused_size ); 62 63 63 64 // Get most used media (top 5) - comprehensive usage tracking. 64 $media_tracker_most_used = get_transient('media_tracker_most_used_media_stats'); 65 66 if (false === $media_tracker_most_used) { 67 $media_tracker_most_used = array(); 68 if (class_exists('\Media_Tracker\Admin\Media_Usage')) { 69 $media_tracker_most_used = \Media_Tracker\Admin\Media_Usage::get_dashboard_most_used_media(); 70 } 71 set_transient('media_tracker_most_used_media_stats', $media_tracker_most_used, HOUR_IN_SECONDS); 72 } 65 $media_tracker_most_used_raw = get_transient( 'media_tracker_most_used_media_stats' ); 66 $media_tracker_is_cached = ( false !== $media_tracker_most_used_raw ); 67 $media_tracker_most_used = $media_tracker_is_cached ? $media_tracker_most_used_raw : array(); 73 68 ?> 74 69 … … 76 71 <div class="media-header"> 77 72 <div class="section-title"> 78 <h2><i class="dashicons dashicons-admin-home"></i> <?php esc_html_e( 'Dashboard', 'media-tracker'); ?></h2>73 <h2><i class="dashicons dashicons-admin-home"></i> <?php esc_html_e( 'Dashboard', 'media-tracker' ); ?></h2> 79 74 <p class="page-subtitle"> 80 <?php esc_html_e( 'Manage unused media, duplicate, optimization and cloud offload from a clean dashboard with Media Tracker.', 'media-tracker'); ?>75 <?php esc_html_e( 'Manage unused media, duplicate, optimization and cloud offload from a clean dashboard with Media Tracker.', 'media-tracker' ); ?> 81 76 </p> 82 77 </div> … … 88 83 <h3 style="display: flex; align-items: center; gap: 8px;"> 89 84 <i class="dashicons dashicons-format-image" style="color: #6366f1;"></i> 90 <?php esc_html_e( 'Unused Media', 'media-tracker'); ?>85 <?php esc_html_e( 'Unused Media', 'media-tracker' ); ?> 91 86 </h3> 92 87 <span class="value"> 93 88 <?php 94 89 /* translators: %d: Number of unused files. */ 95 printf( esc_html__('%d Files', 'media-tracker'), intval($media_tracker_unused_count));96 ?> 97 </span> 98 99 <?php if ( $media_tracker_unused_size > 0): ?>100 <span style="color: #64748b; font-size: 12px;">101 <?php102 /* translators: %s: Formatted file size (e.g. 1.5 MB). */103 printf(esc_html__('Potential saving: %s', 'media-tracker'), esc_html($media_tracker_unused_size_formatted));104 ?>105 </span>90 printf( esc_html__( '%d Files', 'media-tracker' ), intval( $media_tracker_unused_count ) ); 91 ?> 92 </span> 93 94 <?php if ( $media_tracker_unused_size > 0 ) : ?> 95 <span style="color: #64748b; font-size: 12px;"> 96 <?php 97 /* translators: %s: Formatted file size (e.g. 1.5 MB). */ 98 printf( esc_html__( 'Potential saving: %s', 'media-tracker' ), esc_html( $media_tracker_unused_size_formatted ) ); 99 ?> 100 </span> 106 101 <?php endif; ?> 107 102 </div> … … 110 105 <h3 style="display: flex; align-items: center; gap: 8px;"> 111 106 <i class="dashicons dashicons-images-alt2" style="color: #6366f1;"></i> 112 <?php esc_html_e( 'Duplicates Found', 'media-tracker'); ?>107 <?php esc_html_e( 'Duplicates Found', 'media-tracker' ); ?> 113 108 </h3> 114 109 <span class="value"> 115 110 <?php 116 if ( !$media_tracker_has_scanned) {117 echo '<a href="' . esc_url( admin_url('upload.php?page=media-tracker&tab=duplicates')) . '" style="font-size:14px; text-decoration:none;">' . esc_html__('Scan Required', 'media-tracker') . '</a>';111 if ( ! $media_tracker_has_scanned ) { 112 echo '<a href="' . esc_url( admin_url( 'upload.php?page=media-tracker&tab=duplicates' ) ) . '" style="font-size:14px; text-decoration:none;">' . esc_html__( 'Scan Required', 'media-tracker' ) . '</a>'; 118 113 } else { 119 114 /* translators: %d: Number of duplicate files. */ 120 printf( esc_html__('%d Files', 'media-tracker'), intval($media_tracker_duplicate_count));115 printf( esc_html__( '%d Files', 'media-tracker' ), intval( $media_tracker_duplicate_count ) ); 121 116 } 122 117 ?> 123 118 </span> 124 119 <span style="color: #64748b; font-size: 12px;"> 125 <?php esc_html_e( 'Based on file hash matching', 'media-tracker'); ?>120 <?php esc_html_e( 'Based on file hash matching', 'media-tracker' ); ?> 126 121 </span> 127 122 </div> … … 130 125 <h3 style="display: flex; align-items: center; gap: 8px;"> 131 126 <i class="dashicons dashicons-admin-media" style="color: #6366f1;"></i> 132 <?php esc_html_e( 'Total Media', 'media-tracker'); ?>127 <?php esc_html_e( 'Total Media', 'media-tracker' ); ?> 133 128 </h3> 134 129 <span class="value"> 135 130 <?php 136 131 /* translators: %d: Total number of media files. */ 137 printf(esc_html__('%d Files', 'media-tracker'), intval($media_tracker_total_attachments)); 138 ?> 139 </span> 140 <span 141 style="color: #64748b; font-size: 12px;"><?php esc_html_e('Total files in library', 'media-tracker'); ?></span> 132 printf( esc_html__( '%d Files', 'media-tracker' ), intval( $media_tracker_total_attachments ) ); 133 ?> 134 </span> 135 <span style="color: #64748b; font-size: 12px;"> 136 <?php esc_html_e( 'Total files in library', 'media-tracker' ); ?> 137 </span> 142 138 </div> 143 139 </div> … … 147 143 <div class="section-title"> 148 144 <i class="dashicons dashicons-superhero"></i> 149 <?php esc_html_e( 'Quick Actions', 'media-tracker'); ?>145 <?php esc_html_e( 'Quick Actions', 'media-tracker' ); ?> 150 146 </div> 151 147 152 148 <div class="setting-item" style="margin-top: 10px;"> 153 149 <div> 154 <strong><?php esc_html_e('Scan for Unused Media', 'media-tracker'); ?></strong> 155 <p style="font-size: 12px; color: #64748b;"> 156 <?php esc_html_e('Scan all content to find unused media files.', 'media-tracker'); ?> 157 </p> 150 <strong><?php esc_html_e( 'Scan for Unused Media', 'media-tracker' ); ?></strong> 151 <p style="font-size: 12px; color: #64748b;"><?php esc_html_e( 'Scan all content to find unused media files.', 'media-tracker' ); ?></p> 158 152 </div> 159 <button id="mt-scan-now-btn" class="btn btn-primary" style="padding: 6px 12px; font-size: 12px;">160 <?php esc_html_e( 'Scan Now', 'media-tracker'); ?>153 <button class="btn btn-primary" style="padding: 6px 12px; font-size: 12px;" onclick="location.href='<?php echo esc_url( admin_url( 'upload.php?page=media-tracker&tab=unused-media' ) ); ?>'"> 154 <?php esc_html_e( 'Scan', 'media-tracker' ); ?> 161 155 </button> 162 </div>163 <div id="mt-scan-progress-container"164 style="display:none; margin-top: 10px; background: #f1f5f9; border-radius: 4px; overflow: hidden; padding: 5px;">165 <div style="background: #e2e8f0; border-radius: 4px; overflow: hidden; height: 10px;">166 <div id="mt-scan-progress-bar"167 style="width: 0%; height: 100%; background: #6366f1; transition: width 0.3s;"></div>168 </div>169 <div id="mt-scan-progress-text"170 style="text-align: center; font-size: 11px; margin-top: 4px; color: #64748b;">0%</div>171 156 </div> 172 157 173 158 <div class="setting-item"> 174 159 <div> 175 <strong><?php esc_html_e('Find Duplicates', 'media-tracker'); ?></strong> 176 <p style="font-size: 12px; color: #64748b;"> 177 <?php esc_html_e('Detects duplicate images using file hash matching.', 'media-tracker'); ?> 178 </p> 160 <strong><?php esc_html_e( 'Find Duplicates', 'media-tracker' ); ?></strong> 161 <p style="font-size: 12px; color: #64748b;"><?php esc_html_e( 'Detects duplicate images using file hash matching.', 'media-tracker' ); ?></p> 179 162 </div> 180 <button class="btn btn-primary" style="padding: 6px 12px; font-size: 12px;" 181 onclick="location.href='<?php echo esc_url(admin_url('upload.php?page=media-tracker&tab=duplicates')); ?>'"> 182 <?php esc_html_e('Find', 'media-tracker'); ?> 163 <button class="btn btn-primary" style="padding: 6px 12px; font-size: 12px;" onclick="location.href='<?php echo esc_url( admin_url( 'upload.php?page=media-tracker&tab=duplicates' ) ); ?>'"> 164 <?php esc_html_e( 'Find', 'media-tracker' ); ?> 183 165 </button> 184 166 </div> … … 186 168 <div class="setting-item"> 187 169 <div> 188 <strong><?php esc_html_e( 'Bulk Delete Unused', 'media-tracker'); ?></strong>170 <strong><?php esc_html_e( 'Bulk Delete Unused', 'media-tracker' ); ?></strong> 189 171 <p style="font-size: 12px; color: #64748b;"> 190 172 <?php 191 173 /* translators: %d: Number of unused files. */ 192 printf( esc_html__('%d unused files found. Delete safely after backup.', 'media-tracker'), intval($media_tracker_unused_count));174 printf( esc_html__( '%d unused files found. Delete safely after backup.', 'media-tracker' ), intval( $media_tracker_unused_count ) ); 193 175 ?> 194 176 </p> 195 177 </div> 196 <button class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;" 197 onclick="location.href='<?php echo esc_url(admin_url('upload.php?page=media-tracker&tab=unused-media')); ?>'"> 198 <?php esc_html_e('Delete', 'media-tracker'); ?> 178 <button class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="location.href='<?php echo esc_url( admin_url( 'upload.php?page=media-tracker&tab=unused-media' ) ); ?>'"> 179 <?php esc_html_e( 'Delete', 'media-tracker' ); ?> 199 180 </button> 200 181 </div> … … 204 185 <div class="section-title"> 205 186 <i class="dashicons dashicons-chart-bar"></i> 206 <?php esc_html_e( 'Media Statistics', 'media-tracker'); ?>187 <?php esc_html_e( 'Media Statistics', 'media-tracker' ); ?> 207 188 </div> 208 189 <div class="stacked"> … … 210 191 // Get media type statistics. 211 192 $media_tracker_mime_types = array(); 212 if ( class_exists('\Media_Tracker\Admin\Media_Usage')) {193 if ( class_exists( '\Media_Tracker\Admin\Media_Usage' ) ) { 213 194 $media_tracker_mime_types = \Media_Tracker\Admin\Media_Usage::get_mime_type_stats(); 214 195 } 215 196 ?> 216 197 217 <?php if (!empty($media_tracker_mime_types)): ?> 218 <?php foreach ($media_tracker_mime_types as $media_tracker_mime): ?> 219 <div 220 style="display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8fafc; border-radius: 8px;"> 221 <i class="dashicons dashicons-media-default" 222 style="color: #6366f1; width: 20px; text-align: center;"></i> 198 <?php if ( ! empty( $media_tracker_mime_types ) ) : ?> 199 <?php foreach ( $media_tracker_mime_types as $media_tracker_mime ) : ?> 200 <div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8fafc; border-radius: 8px;"> 201 <i class="dashicons dashicons-media-default" style="color: #6366f1; width: 20px; text-align: center;"></i> 223 202 <div style="flex: 1;"> 224 203 <div style="font-size: 14px; font-weight: 500;"> 225 <?php echo esc_html( str_replace('image/', '', $media_tracker_mime->post_mime_type)); ?>204 <?php echo esc_html( str_replace( 'image/', '', $media_tracker_mime->post_mime_type ) ); ?> 226 205 </div> 227 206 <div class="sub-label"> 228 207 <?php 229 208 /* translators: %d: Number of files for a specific mime type. */ 230 printf( esc_html__('%d files', 'media-tracker'), intval($media_tracker_mime->count));209 printf( esc_html__( '%d files', 'media-tracker' ), intval( $media_tracker_mime->count ) ); 231 210 ?> 232 211 </div> … … 234 213 </div> 235 214 <?php endforeach; ?> 236 <?php else : ?>215 <?php else : ?> 237 216 <p style="color: #64748b; font-size: 12px; text-align: center; padding: 20px;"> 238 <?php esc_html_e( 'No media files found yet.', 'media-tracker'); ?>217 <?php esc_html_e( 'No media files found yet.', 'media-tracker' ); ?> 239 218 </p> 240 219 <?php endif; ?> … … 242 221 </div> 243 222 </div> 244 245 246 223 247 224 <div class="card" style="margin-top: 1.5rem;"> 248 225 <div class="section-title"> 249 226 <i class="dashicons dashicons-chart-area"></i> 250 <?php esc_html_e('Most Used Media', 'media-tracker'); ?> 251 </div> 252 <?php if (!empty($media_tracker_most_used)): ?> 253 <table class="mt-overview-table"> 254 <thead> 255 <tr> 256 <th><?php esc_html_e('File Name', 'media-tracker'); ?></th> 257 <th><?php esc_html_e('Type', 'media-tracker'); ?></th> 258 <th><?php esc_html_e('Usage Count', 'media-tracker'); ?></th> 259 <th><?php esc_html_e('Actions', 'media-tracker'); ?></th> 260 </tr> 261 </thead> 262 <tbody> 263 <?php foreach ($media_tracker_most_used as $media_tracker_media): ?> 264 <?php 265 $media_tracker_file_type = $media_tracker_media->post_mime_type ? str_replace('image/', '', $media_tracker_media->post_mime_type) : '-'; 266 $media_tracker_edit_link = get_edit_post_link($media_tracker_media->ID); 267 $media_tracker_view_link = get_permalink($media_tracker_media->ID); 268 ?> 269 <tr> 270 <td><strong><?php echo esc_html($media_tracker_media->post_title); ?></strong></td> 271 <td><?php echo esc_html($media_tracker_file_type); ?></td> 272 <td> 273 <span class="tag" style="background: #10b981; color: white;"> 274 <?php 275 /* translators: %d: Number of times the media is used. */ 276 printf(esc_html__('%d times', 'media-tracker'), intval($media_tracker_media->usage_count)); 277 ?> 278 </span> 279 </td> 280 <td> 281 <a href="<?php echo esc_url($media_tracker_edit_link); ?>" class="btn btn-outline" 282 style="padding: 5px 10px; text-decoration: none;"> 283 <i class="dashicons dashicons-visibility"></i> 284 <?php esc_html_e('View', 'media-tracker'); ?> 285 </a> 286 </td> 287 </tr> 288 <?php endforeach; ?> 289 </tbody> 290 </table> 291 <?php else: ?> 292 <p style="color: #64748b; text-align: center; padding: 40px;"> 293 <i class="dashicons dashicons-chart-bar" 294 style="font-size: 48px; color: #cbd5e1; margin-bottom: 15px; height: 48px; width: 48px;"></i><br> 295 <?php esc_html_e('No media usage data available yet.', 'media-tracker'); ?> 296 </p> 297 <?php endif; ?> 227 <?php esc_html_e( 'Most Used Media', 'media-tracker' ); ?> 228 </div> 229 230 <div id="media-tracker-most-used-container"> 231 <?php if ( $media_tracker_is_cached ) : ?> 232 <?php if ( ! empty( $media_tracker_most_used ) ) : ?> 233 <table class="mt-overview-table"> 234 <thead> 235 <tr> 236 <th><?php esc_html_e( 'File Name', 'media-tracker' ); ?></th> 237 <th><?php esc_html_e( 'Type', 'media-tracker' ); ?></th> 238 <th><?php esc_html_e( 'Usage Count', 'media-tracker' ); ?></th> 239 <th style="text-align: center;"><?php esc_html_e( 'Actions', 'media-tracker' ); ?></th> 240 </tr> 241 </thead> 242 <tbody> 243 <?php foreach ( $media_tracker_most_used as $media_tracker_media ) : ?> 244 <?php 245 $media_tracker_file_type = $media_tracker_media->post_mime_type ? str_replace( 'image/', '', $media_tracker_media->post_mime_type ) : '-'; 246 $media_tracker_edit_link = get_edit_post_link( $media_tracker_media->ID ); 247 $media_tracker_view_link = get_permalink( $media_tracker_media->ID ); 248 ?> 249 <tr> 250 <td><strong><?php echo esc_html( $media_tracker_media->post_title ); ?></strong></td> 251 <td><?php echo esc_html( $media_tracker_file_type ); ?></td> 252 <td> 253 <span class="tag" style="background: #10b981; color: white;"> 254 <?php 255 /* translators: %d: Number of times the media is used. */ 256 printf( esc_html__( '%d times', 'media-tracker' ), intval( $media_tracker_media->usage_count ) ); 257 ?> 258 </span> 259 </td> 260 <td> 261 <a href="<?php echo esc_url( $media_tracker_edit_link ); ?>" class="btn btn-outline" style="padding: 5px 10px; text-decoration: none;"> 262 <i class="dashicons dashicons-visibility"></i> 263 <?php esc_html_e( 'View', 'media-tracker' ); ?> 264 </a> 265 </td> 266 </tr> 267 <?php endforeach; ?> 268 </tbody> 269 </table> 270 <?php else : ?> 271 <p style="color: #64748b; text-align: center; padding: 40px;"> 272 <i class="dashicons dashicons-chart-bar" style="font-size: 48px; color: #cbd5e1; margin-bottom: 15px; height: 48px; width: 48px;"></i><br> 273 <?php esc_html_e( 'No media usage data available.', 'media-tracker' ); ?> 274 </p> 275 <?php endif; ?> 276 <?php else : ?> 277 <div id="media-tracker-loading-stats" style="text-align: center; padding: 40px;"> 278 <span class="spinner is-active" style="float:none; margin:0 10px 0 0;"></span> 279 <?php esc_html_e( 'Loading usage statistics...', 'media-tracker' ); ?> 280 </div> 281 <script type="text/javascript"> 282 jQuery(document).ready(function($) { 283 $.ajax({ 284 url: ajaxurl, 285 type: 'POST', 286 data: { 287 action: 'media_tracker_get_most_used' 288 }, 289 success: function(response) { 290 if (response.success) { 291 $('#media-tracker-most-used-container').html(response.data.html); 292 } else { 293 $('#media-tracker-most-used-container').html('<p style="padding:20px;text-align:center;">' + (response.data || 'Error loading stats.') + '</p>'); 294 } 295 }, 296 error: function() { 297 $('#media-tracker-most-used-container').html('<p style="padding:20px;text-align:center;">Error loading stats.</p>'); 298 } 299 }); 300 }); 301 </script> 302 <?php endif; ?> 303 </div> 298 304 </div> -
media-tracker/trunk/includes/Admin/views/unused-media-list.php
r3454648 r3457950 72 72 var clientProgress = 0; // Client-side progress simulation 73 73 var targetProgress = 0; // Target progress from server 74 var currentStepName = 'Starting...'; 74 75 var progressAnimInterval = null; // Animation interval 75 76 … … 147 148 // Smooth progress animation function 148 149 function animateProgress(){ 150 // Simulate continuous progress even if server is slow (Fake Progress) 151 // Stop auto-incrementing if we reach 99% to wait for actual completion 152 if (targetProgress < 99 && clientProgress >= targetProgress - 1) { 153 targetProgress += 0.2; 154 } 155 149 156 // Gradually move clientProgress towards targetProgress 150 157 if (clientProgress < targetProgress) { 151 158 // Increase gradually based on distance 152 159 var diff = targetProgress - clientProgress; 153 var increment = diff > 20 ? 3 : (diff > 10 ? 2 : 1); // Faster when far, slower when close 160 // Smoother increments for float values 161 var increment = diff > 30 ? 2 : (diff > 10 ? 1 : 0.2); 154 162 155 163 clientProgress += increment; … … 158 166 } 159 167 $('#media-scan-progress-fill').css('width', clientProgress + '%'); 168 $('#media-scan-progress-text').html('Scan status: ' + currentStepName + ' (' + Math.floor(clientProgress) + '%)'); 160 169 } 161 170 } … … 173 182 var d = res.data; 174 183 var pct = Math.max(0, Math.min(100, parseInt(d.percentage, 10) || 0)); 175 targetProgress = pct; // Update target from server 176 177 $('#media-scan-progress-text').text('Scan status: ' + (d.current_step || '') + ' (' + pct + '%)'); 184 185 // Prevent regression: only update target if server reports higher progress 186 targetProgress = Math.max(targetProgress, pct); 187 188 if (d.current_step) { 189 currentStepName = d.current_step; 190 } 191 192 // If we are caught up, update text immediately 193 if (clientProgress >= targetProgress) { 194 $('#media-scan-progress-text').html('Scan status: ' + currentStepName + ' (' + Math.round(clientProgress) + '%)'); 195 } 178 196 179 197 // Fallback: if scan appears stalled at start, trigger synchronous scan … … 199 217 targetProgress = 100; 200 218 $('#media-scan-progress-fill').css('width', '100%'); 219 220 // Remove tab closing prevention 221 $(window).off('beforeunload.mediaTrackerScan'); 201 222 202 223 // Clear progress transient to avoid sticky progress UI … … 263 284 clientProgress = 0; 264 285 targetProgress = 5; // Start with 5% 286 currentStepName = 'Starting...'; 265 287 syncTriggered = false; 266 288 stuckChecks = 0; … … 272 294 $('#media-scan-progress-fill').css('width', '0%'); 273 295 $('#media-scan-progress-text').text('Scan status: Starting... (0%)'); 274 $btn.prop('disabled', true).text('Starting...'); 296 $btn.prop('disabled', true).html('<span class="spinner is-active" style="float:none; margin:0 5px 0 0; vertical-align:middle;"></span> Starting...'); 297 298 // Prevent tab closing 299 $(window).on('beforeunload.mediaTrackerScan', function() { 300 return 'Scanning in progress. Please do not close this tab.'; 301 }); 275 302 276 303 $.post(AJAX_URL, { … … 278 305 nonce: NONCE 279 306 }).done(function(res){ 280 $btn. text('Scanning...');307 $btn.html('<span class="spinner is-active" style="float:none; margin:0 5px 0 0; vertical-align:middle;"></span> Scanning...'); 281 308 282 309 // Start smooth animation immediately … … 292 319 }, 3000); 293 320 ensureScanButtonExists('Scan Unused Media').prop('disabled', false); 321 322 // Remove tab closing prevention 323 $(window).off('beforeunload.mediaTrackerScan'); 294 324 }); 295 325 -
media-tracker/trunk/includes/Assets.php
r3454648 r3457950 42 42 ); 43 43 44 // Only load on Media Tracker page (upload.php?page=media-tracker)45 if ( 'media_page_media-tracker' === $hook) {44 // Only load on Media Tracker pages (all subpages) 45 if ( strpos( $hook, 'media_page_media-tracker' ) === 0 ) { 46 46 $data['base_url'] = admin_url( 'upload.php?page=media-tracker' ); 47 47 -
media-tracker/trunk/includes/Installer.php
r3455634 r3457950 3 3 namespace Media_Tracker; 4 4 5 defined( 'ABSPATH') || exit;5 defined( 'ABSPATH' ) || exit; 6 6 7 7 /** 8 8 * Installer class 9 9 */ 10 class Installer 11 { 10 class Installer { 12 11 13 12 /** … … 19 18 * @return void 20 19 */ 21 public function run() 22 { 20 public function run() { 23 21 $this->add_version(); 24 $this->create_index_table();25 22 $this->optimize_database_indexes(); 26 23 $this->schedule_cron_jobs(); 27 }28 29 /**30 * Create the custom index table for media relationships31 *32 * @since 1.4.033 */34 public function create_index_table()35 {36 global $wpdb;37 38 $table_name = $wpdb->prefix . 'media_tracker_index';39 $charset_collate = $wpdb->get_charset_collate();40 41 $sql = "CREATE TABLE $table_name (42 id bigint(20) NOT NULL AUTO_INCREMENT,43 media_id bigint(20) NOT NULL,44 used_in bigint(20) NOT NULL,45 context varchar(50) DEFAULT 'content' NOT NULL,46 last_scanned datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,47 PRIMARY KEY (id),48 KEY media_id (media_id),49 KEY used_in (used_in),50 KEY media_used_composite (media_id, used_in)51 ) $charset_collate;";52 53 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');54 dbDelta($sql);55 24 } 56 25 … … 63 32 * @return void 64 33 */ 65 public function schedule_cron_jobs() 66 { 34 public function schedule_cron_jobs() { 67 35 // Schedule the batch processing cron job 68 36 if (!wp_next_scheduled('media_tracker_batch_process')) { … … 85 53 * @return void 86 54 */ 87 public static function clear_cron_jobs() 88 { 55 public static function clear_cron_jobs() { 89 56 // Clear the batch processing cron job 90 57 $timestamp = wp_next_scheduled('media_tracker_batch_process'); … … 105 72 * @return void 106 73 */ 107 public function optimize_database_indexes() 108 { 74 public function optimize_database_indexes() { 109 75 global $wpdb; 110 76 111 77 // Check if indexes already exist to avoid duplicate creation 112 $indexes_created = get_option( 'media_tracker_indexes_created', false);78 $indexes_created = get_option( 'media_tracker_indexes_created', false ); 113 79 114 if ( !$indexes_created) {80 if ( ! $indexes_created ) { 115 81 // MySQL-compatible indexes (no partial WHERE clauses) 116 82 // Speed up duplicate hash aggregation: index on meta_key and meta_value prefix … … 119 85 120 86 // Check if postmeta index exists - direct query necessary as no WP API available 121 $existing_postmeta_idx = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->postmeta} WHERE Key_name = 'idx_postmeta_media_tracker_hash'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching122 if ( empty($existing_postmeta_idx)) {87 $existing_postmeta_idx = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->postmeta} WHERE Key_name = 'idx_postmeta_media_tracker_hash'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 88 if ( empty( $existing_postmeta_idx ) ) { 123 89 // Create index for performance optimization - direct query necessary as no WP API available 124 $wpdb->query( "CREATE INDEX idx_postmeta_media_tracker_hash ON {$wpdb->postmeta} (meta_key, meta_value(64))"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange90 $wpdb->query( "CREATE INDEX idx_postmeta_media_tracker_hash ON {$wpdb->postmeta} (meta_key, meta_value(64))" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange 125 91 } 126 92 127 93 // Check if posts index exists - direct query necessary as no WP API available 128 $existing_posts_idx = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->posts} WHERE Key_name = 'idx_posts_type_status'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching129 if ( empty($existing_posts_idx)) {94 $existing_posts_idx = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->posts} WHERE Key_name = 'idx_posts_type_status'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 95 if ( empty( $existing_posts_idx ) ) { 130 96 // Create index for performance optimization - direct query necessary as no WP API available 131 $wpdb->query( "CREATE INDEX idx_posts_type_status ON {$wpdb->posts} (post_type, post_status)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange97 $wpdb->query( "CREATE INDEX idx_posts_type_status ON {$wpdb->posts} (post_type, post_status)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange 132 98 } 133 99 134 100 // New: index to accelerate attachment/mime filtering used by duplicate queries 135 101 // Check if posts mime index exists - direct query necessary as no WP API available 136 $existing_posts_mime_idx = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->posts} WHERE Key_name = 'idx_posts_type_mime'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching137 if ( empty($existing_posts_mime_idx)) {102 $existing_posts_mime_idx = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->posts} WHERE Key_name = 'idx_posts_type_mime'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 103 if ( empty( $existing_posts_mime_idx ) ) { 138 104 // Create index for performance optimization - direct query necessary as no WP API available 139 $wpdb->query( "CREATE INDEX idx_posts_type_mime ON {$wpdb->posts} (post_type, post_mime_type)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange105 $wpdb->query( "CREATE INDEX idx_posts_type_mime ON {$wpdb->posts} (post_type, post_mime_type)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange 140 106 } 141 107 142 update_option( 'media_tracker_indexes_created', true);108 update_option( 'media_tracker_indexes_created', true ); 143 109 } 144 110 } … … 152 118 * @return void 153 119 */ 154 public function add_version() 155 { 156 $installed = get_option('media_tracker_installed'); 120 public function add_version() { 121 $installed = get_option( 'media_tracker_installed' ); 157 122 158 if ( !$installed) {159 update_option( 'media_tracker_installed', time());123 if ( ! $installed ) { 124 update_option( 'media_tracker_installed', time() ); 160 125 } 161 126 162 update_option( 'media_tracker_version', MEDIA_TRACKER_VERSION);127 update_option( 'media_tracker_version', MEDIA_TRACKER_VERSION ); 163 128 } 164 129 165 public static function deactivate() 166 { 167 add_action('admin_footer', array(__CLASS__, 'feedback_modal_html')); 130 public static function deactivate() { 131 add_action( 'admin_footer', array( __CLASS__, 'feedback_modal_html' ) ); 168 132 } 169 133 … … 171 135 * AJAX handler to save feedback 172 136 */ 173 public static function save_feedback() 174 { 175 check_ajax_referer('media_tracker_nonce', 'nonce'); 137 public static function save_feedback() { 138 check_ajax_referer( 'media_tracker_nonce', 'nonce' ); 176 139 177 140 $feedback = isset($_POST['feedback']) ? sanitize_textarea_field(wp_unslash($_POST['feedback'])) : ''; 178 141 179 if ( !empty($feedback)) {142 if ( ! empty( $feedback ) ) { 180 143 $to = '[email protected]'; 181 $subject = __( 'Media Tracker Plugin Feedback', 'media-tracker');144 $subject = __( 'Media Tracker Plugin Feedback', 'media-tracker' ); 182 145 $message = "Feedback:\n\n" . $feedback; 183 $headers = array( 'Content-Type: text/plain; charset=UTF-8');146 $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); 184 147 185 wp_mail( $to, $subject, $message, $headers);148 wp_mail( $to, $subject, $message, $headers ); 186 149 187 150 wp_send_json_success(); … … 192 155 * Output HTML for feedback modal 193 156 */ 194 public static function feedback_modal_html() 195 { ?> 157 public static function feedback_modal_html() { ?> 196 158 <div id="mt-feedback-modal"> 197 159 <div class="mt-feedback-modal-content"> 198 160 <header class="mt-feedback-modal-header"> 199 161 <span class="close">×</span> 200 <h3><?php esc_html_e("If you have a moment, we'd love to know why you're deactivating the Media Tracker plugin!", "media-tracker"); ?> 201 </h3> 162 <h3><?php esc_html_e( "If you have a moment, we'd love to know why you're deactivating the Media Tracker plugin!", "media-tracker" ); ?></h3> 202 163 </header> 203 164 204 165 <div class="mt-feedback-modal-body"> 205 <textarea name="feedback" 206 placeholder="<?php esc_html_e('Enter your feedback here...', 'media-tracker') ?>"></textarea> 166 <textarea name="feedback" placeholder="<?php esc_html_e( 'Enter your feedback here...', 'media-tracker' ) ?>"></textarea> 207 167 </div> 208 168 209 169 <footer class="mt-feedback-modal-footer"> 210 <button id="mt-skip-feedback"><?php esc_html_e( 'Skip & Deactivate', 'media-tracker'); ?></button>211 <button id="mt-submit-feedback"><?php esc_html_e( 'Submit & Deactivate', 'media-tracker'); ?></button>170 <button id="mt-skip-feedback"><?php esc_html_e( 'Skip & Deactivate', 'media-tracker' ); ?></button> 171 <button id="mt-submit-feedback"><?php esc_html_e( 'Submit & Deactivate', 'media-tracker' ); ?></button> 212 172 </footer> 213 173 </div> -
media-tracker/trunk/includes/functions.php
r3454648 r3457950 27 27 'icon' => 'dashicons dashicons-admin-home', 28 28 'badge' => '', 29 'is_link' => false,30 'url' => '',29 'is_link' => true, 30 'url' => admin_url( 'upload.php?page=media-tracker-overview' ), 31 31 'active' => false, 32 32 ), … … 35 35 'icon' => 'dashicons dashicons-format-image', 36 36 'badge' => '', 37 'is_link' => false,38 'url' => '',37 'is_link' => true, 38 'url' => admin_url( 'upload.php?page=media-tracker-unused-media' ), 39 39 'active' => false, 40 40 ), … … 43 43 'icon' => 'dashicons dashicons-images-alt', 44 44 'badge' => '', 45 'is_link' => false,46 'url' => '',45 'is_link' => true, 46 'url' => admin_url( 'upload.php?page=media-tracker-duplicates' ), 47 47 'active' => false, 48 48 ), … … 51 51 'icon' => 'dashicons dashicons-cloud-upload', 52 52 'badge' => '', 53 'is_link' => false,54 'url' => '',53 'is_link' => true, 54 'url' => admin_url( 'upload.php?page=media-tracker-external-storage' ), 55 55 'active' => false, 56 56 ), … … 58 58 'label' => __( 'Optimization', 'media-tracker' ), 59 59 'icon' => 'dashicons dashicons-performance', 60 'is_link' => false, 61 'url' => '', 60 'badge' => '', 61 'is_link' => true, 62 'url' => admin_url( 'upload.php?page=media-tracker-optimization' ), 62 63 'active' => false, 63 64 ), … … 66 67 'icon' => 'dashicons dashicons-lock', 67 68 'badge' => '', 68 'is_link' => false,69 'url' => '',69 'is_link' => true, 70 'url' => admin_url( 'upload.php?page=media-tracker-security' ), 70 71 'active' => false, 71 72 ), … … 74 75 'icon' => 'dashicons dashicons-admin-multisite', 75 76 'badge' => '', 76 'is_link' => false,77 'url' => '',77 'is_link' => true, 78 'url' => admin_url( 'upload.php?page=media-tracker-multisite' ), 78 79 'active' => false, 79 80 'class' => 'multisite' … … 83 84 'icon' => 'dashicons dashicons-media-document', 84 85 'badge' => '', 85 'is_link' => false,86 'url' => '',86 'is_link' => true, 87 'url' => admin_url( 'upload.php?page=media-tracker-documents' ), 87 88 'active' => false, 88 89 ), … … 97 98 ), 98 99 ); 100 101 // Add license menu item if Pro is active 102 if ( media_tracker_is_pro_active() ) { 103 $menu_items['license'] = array( 104 'label' => __( 'License', 'media-tracker' ), 105 'icon' => 'dashicons dashicons-admin-network', 106 'badge' => '', 107 'is_link' => true, 108 'url' => admin_url( 'upload.php?page=media-tracker-license' ), 109 'active' => false, 110 ); 111 } 99 112 100 113 // Set active state for current tab … … 113 126 114 127 /** 115 * Get current active tab from URL parameter 128 * Get current active tab from URL parameter or page parameter 116 129 * 117 130 * @since 1.3.0 … … 119 132 */ 120 133 function media_tracker_get_current_tab() { 121 // Check URL parameter 'tab' 134 $tab = ''; 135 136 // First check URL parameter 'tab' (for backward compatibility) 122 137 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for determining the current tab. 123 $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; 124 125 // If empty, check if tab name exists in URL query string 138 if ( isset( $_GET['tab'] ) ) { 139 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for determining the current tab. 140 $tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) ); 141 } 142 143 // If no tab parameter, check page parameter (for new page-based navigation) 144 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for determining the current tab. 145 if ( empty( $tab ) && isset( $_GET['page'] ) ) { 146 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for determining the current tab. 147 $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); 148 149 // Extract tab from page parameter (e.g., 'media-tracker-duplicates' -> 'duplicates') 150 if ( strpos( $page, 'media-tracker-' ) === 0 ) { 151 $tab = str_replace( 'media-tracker-', '', $page ); 152 } elseif ( $page === 'media-tracker' ) { 153 // Main page defaults to overview 154 $tab = 'overview'; 155 } 156 } 157 158 // If still empty, check if tab name exists in URL query string (backward compatibility) 126 159 if ( empty( $tab ) ) { 127 $known_tabs = array( 'overview', 'unused-media', 'duplicates', 'external-storage', 'optimization', 'security', 'multisite', ' settings', 'license' );160 $known_tabs = array( 'overview', 'unused-media', 'duplicates', 'external-storage', 'optimization', 'security', 'multisite', 'license' ); 128 161 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for reading the query string. 129 162 $query_string = isset( $_SERVER['QUERY_STRING'] ) ? sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : ''; … … 299 332 300 333 if ( ! empty( $item['is_link'] ) && ! empty( $item['url'] ) ) { 301 // For external links 302 $custom_class = ! empty( $item['class'] ) ? ' class="' . esc_attr( $item['class'] ) . '"' : ''; 334 // For links (internal pages or external) 335 $classes = array(); 336 337 // Add active class if needed 338 if ( ! empty( $item['active'] ) ) { 339 $classes[] = 'active'; 340 } 341 342 // Add custom class if exists 343 if ( ! empty( $item['class'] ) ) { 344 $classes[] = $item['class']; 345 } 346 347 // Build class attribute 348 $custom_class = ! empty( $classes ) ? ' class="' . esc_attr( implode( ' ', $classes ) ) . '"' : ''; 303 349 $target_attr = ! empty( $item['target'] ) ? ' target="' . esc_attr( $item['target'] ) . '"' : ''; 304 350 305 351 printf( 306 '<li><a href="%s"%s%s><i class="%s"></i> %s%s</a></li>', 352 '<li%s><a href="%s"%s%s><i class="%s"></i> %s%s</a></li>', 353 $custom_class, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Class attribute is constructed with esc_attr. 307 354 esc_url( $item['url'] ), 308 355 $custom_class, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Class attribute is constructed with esc_attr. … … 313 360 ); 314 361 } else { 315 // For regular tabs 362 // For regular tabs (non-link items, if any) 316 363 $classes = array(); 317 364 -
media-tracker/trunk/languages/media-tracker.pot
r3455634 r3457950 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Media Tracker 1.3. 0\n"5 "Project-Id-Version: Media Tracker 1.3.2\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-02- 06T19:03:09+00:00\n"12 "POT-Creation-Date: 2026-02-10T11:33: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:10420 19 #: includes/Admin/Menu.php:105 20 #: includes/Admin/Menu.php:106 21 21 msgid "Media Tracker" 22 22 msgstr "" … … 37 37 msgstr "" 38 38 39 #: includes/Admin/Duplicate_Images.php:4 339 #: includes/Admin/Duplicate_Images.php:44 40 40 msgid "Duplicates" 41 41 msgstr "" 42 42 43 #: includes/Admin/Duplicate_Images.php:4 543 #: includes/Admin/Duplicate_Images.php:46 44 44 msgid "Duplicate images filter" 45 45 msgstr "" 46 46 47 #: includes/Admin/Duplicate_Images.php:4 647 #: includes/Admin/Duplicate_Images.php:47 48 48 msgid "All Media" 49 49 msgstr "" 50 50 51 #: includes/Admin/Duplicate_Images.php:4 851 #: includes/Admin/Duplicate_Images.php:49 52 52 msgid "Show Duplicate Images" 53 53 msgstr "" 54 54 55 #: includes/Admin/Duplicate_Images.php:5 355 #: includes/Admin/Duplicate_Images.php:54 56 56 msgid "Re-scan" 57 57 msgstr "" 58 58 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 59 #: includes/Admin/Duplicate_Images.php:377 60 #: includes/Admin/Duplicate_Images.php:565 61 #: includes/Admin/Duplicate_Images.php:620 62 #: includes/Admin/Duplicate_Images.php:654 63 #: includes/Admin/Menu.php:257 63 64 msgid "Unauthorized" 64 65 msgstr "" 65 66 66 67 #. translators: 1: number of hashes reset, 2: number of images being rescanned 67 #: includes/Admin/Duplicate_Images.php:60 068 #: includes/Admin/Duplicate_Images.php:605 68 69 #, php-format 69 70 msgid "Reset %1$d hashes. Re-scanning %2$d images..." 70 71 msgstr "" 71 72 72 #: includes/Admin/Duplicate_Images.php:6 1773 #: includes/Admin/Duplicate_Images.php:660 73 74 msgid "No images selected." 74 75 msgstr "" 75 76 76 77 #. translators: %d: number of deleted images 77 #: includes/Admin/Duplicate_Images.php:6 4378 #: includes/Admin/Duplicate_Images.php:686 78 79 #, php-format 79 80 msgid "Deleted %d duplicate images." 80 81 msgstr "" 81 82 82 #: includes/Admin/Duplicate_Images.php:6 4983 #: includes/Admin/Duplicate_Images.php:692 83 84 msgid "No images were deleted." 84 85 msgstr "" 85 86 86 #: includes/Admin/Duplicate_Images.php: 68487 #: includes/Admin/Duplicate_Images.php:727 87 88 msgid "items" 88 89 msgstr "" 89 90 90 #: includes/Admin/Media_Usage.php:153 91 #: includes/Admin/Media_Usage.php:47 92 #: includes/Admin/views/tabs/tab-overview.php:236 93 msgid "File Name" 94 msgstr "" 95 96 #: includes/Admin/Media_Usage.php:48 97 #: includes/Admin/Media_Usage.php:433 98 #: includes/Admin/views/tabs/tab-overview.php:237 99 msgid "Type" 100 msgstr "" 101 102 #: includes/Admin/Media_Usage.php:49 103 #: includes/Admin/views/tabs/tab-overview.php:238 104 msgid "Usage Count" 105 msgstr "" 106 107 #: includes/Admin/Media_Usage.php:50 108 #: includes/Admin/Media_Usage.php:435 109 #: includes/Admin/views/tabs/tab-duplicates.php:162 110 #: includes/Admin/views/tabs/tab-overview.php:239 111 msgid "Actions" 112 msgstr "" 113 114 #. translators: %d: Number of times the media is used. 115 #: includes/Admin/Media_Usage.php:66 116 #: includes/Admin/views/tabs/tab-overview.php:256 117 #, php-format 118 msgid "%d times" 119 msgstr "" 120 121 #: includes/Admin/Media_Usage.php:73 122 #: includes/Admin/Unused_Media_List.php:83 123 #: includes/Admin/views/tabs/tab-overview.php:263 124 msgid "View" 125 msgstr "" 126 127 #: includes/Admin/Media_Usage.php:84 128 #: includes/Admin/views/tabs/tab-overview.php:273 129 msgid "No media usage data available." 130 msgstr "" 131 132 #: includes/Admin/Media_Usage.php:295 91 133 msgid "Media Usage" 92 134 msgstr "" 93 135 94 #: includes/Admin/Media_Usage.php: 16895 #: includes/Admin/views/tabs/tab-duplicates.php:15 1136 #: includes/Admin/Media_Usage.php:310 137 #: includes/Admin/views/tabs/tab-duplicates.php:155 96 138 msgid "Usages Count" 97 139 msgstr "" 98 140 99 #: includes/Admin/Media_Usage.php: 190141 #: includes/Admin/Media_Usage.php:332 100 142 msgid "Open attachment edit screen" 101 143 msgstr "" 102 144 103 #: includes/Admin/Media_Usage.php: 289145 #: includes/Admin/Media_Usage.php:431 104 146 msgid "#" 105 147 msgstr "" 106 148 107 #: includes/Admin/Media_Usage.php: 290108 #: includes/Admin/views/tabs/tab-duplicates.php:1 36149 #: includes/Admin/Media_Usage.php:432 150 #: includes/Admin/views/tabs/tab-duplicates.php:140 109 151 msgid "Title" 110 152 msgstr "" 111 153 112 #: includes/Admin/Media_Usage.php:291 113 #: includes/Admin/views/tabs/tab-overview.php:257 114 msgid "Type" 115 msgstr "" 116 117 #: includes/Admin/Media_Usage.php:292 154 #: includes/Admin/Media_Usage.php:434 118 155 msgid "Date Added" 119 156 msgstr "" 120 157 121 #: includes/Admin/Media_Usage.php:293 122 #: includes/Admin/views/tabs/tab-duplicates.php:158 123 #: includes/Admin/views/tabs/tab-overview.php:259 124 msgid "Actions" 125 msgstr "" 126 127 #: includes/Admin/Media_Usage.php:306 158 #: includes/Admin/Media_Usage.php:448 128 159 msgid "System Setting" 129 160 msgstr "" 130 161 131 #: includes/Admin/Media_Usage.php: 309162 #: includes/Admin/Media_Usage.php:451 132 163 msgid "Customize Site Icon" 133 164 msgstr "" 134 165 135 166 #. Translators: This is a time difference string 136 #: includes/Admin/Media_Usage.php: 319167 #: includes/Admin/Media_Usage.php:461 137 168 #, php-format 138 169 msgid "%s ago" 139 170 msgstr "" 140 171 141 #: includes/Admin/Media_Usage.php: 341172 #: includes/Admin/Media_Usage.php:483 142 173 msgid "Admin View" 143 174 msgstr "" 144 175 145 #: includes/Admin/Media_Usage.php: 342176 #: includes/Admin/Media_Usage.php:484 146 177 msgid "Frontend View" 147 178 msgstr "" 148 179 149 #: includes/Admin/Media_Usage.php: 350180 #: includes/Admin/Media_Usage.php:492 150 181 msgid "No posts or pages found using this media file." 151 182 msgstr "" 152 183 153 #: includes/Admin/Media_Usage.php:420 154 #: includes/Admin/Media_Usage.php:438 184 #: includes/Admin/Media_Usage.php:533 155 185 msgid "Site Icon (Favicon)" 156 186 msgstr "" 157 187 158 #: includes/Admin/Menu.php:129 188 #: includes/Admin/Menu.php:115 189 #: includes/Admin/Menu.php:116 190 #: includes/Admin/views/tabs/tab-overview.php:73 191 #: includes/functions.php:26 192 msgid "Dashboard" 193 msgstr "" 194 195 #: includes/Admin/Menu.php:124 196 #: includes/Admin/Menu.php:125 197 #: includes/Admin/views/tabs/tab-overview.php:85 198 #: includes/Admin/views/unused-media-list.php:21 199 #: includes/functions.php:34 200 msgid "Unused Media" 201 msgstr "" 202 203 #: includes/Admin/Menu.php:133 204 #: includes/Admin/Menu.php:134 205 #: includes/Admin/views/tabs/tab-duplicates.php:96 206 #: includes/functions.php:42 207 msgid "Duplicate Media" 208 msgstr "" 209 210 #: includes/Admin/Menu.php:142 211 #: includes/Admin/Menu.php:143 212 #: includes/functions.php:50 213 msgid "External Storage" 214 msgstr "" 215 216 #: includes/Admin/Menu.php:151 217 #: includes/Admin/Menu.php:152 218 #: includes/functions.php:58 219 msgid "Optimization" 220 msgstr "" 221 222 #: includes/Admin/Menu.php:160 223 #: includes/Admin/Menu.php:161 224 #: includes/functions.php:66 225 msgid "Security & Logs" 226 msgstr "" 227 228 #: includes/Admin/Menu.php:169 229 #: includes/Admin/Menu.php:170 230 #: includes/functions.php:74 231 msgid "Multi-site" 232 msgstr "" 233 234 #: includes/Admin/Menu.php:178 235 #: includes/Admin/Menu.php:179 236 #: includes/Admin/views/tabs/tab-documents.php:15 237 #: includes/functions.php:83 238 msgid "Documents" 239 msgstr "" 240 241 #: includes/Admin/Menu.php:188 242 #: includes/Admin/Menu.php:189 243 #: includes/Admin/views/tabs/tab-license.php:13 244 #: includes/functions.php:104 245 msgid "License" 246 msgstr "" 247 248 #: includes/Admin/Menu.php:268 159 249 msgid "Cache cleared successfully." 160 250 msgstr "" 161 251 162 #: includes/Admin/Menu.php: 137163 #: includes/Admin/Menu.php: 178164 #: includes/Admin/Menu.php: 217165 #: includes/Admin/Menu.php:4 20166 #: includes/Admin/Menu.php:4 42167 #: includes/Admin/Menu.php: 474252 #: includes/Admin/Menu.php:276 253 #: includes/Admin/Menu.php:317 254 #: includes/Admin/Menu.php:356 255 #: includes/Admin/Menu.php:464 256 #: includes/Admin/Menu.php:486 257 #: includes/Admin/Menu.php:518 168 258 msgid "Unauthorized: You do not have permission to perform this action." 169 259 msgstr "" 170 260 171 #: includes/Admin/Menu.php: 142172 #: includes/Admin/Menu.php:4 25173 #: includes/Admin/Menu.php:4 47174 #: includes/Admin/Menu.php: 479261 #: includes/Admin/Menu.php:281 262 #: includes/Admin/Menu.php:469 263 #: includes/Admin/Menu.php:491 264 #: includes/Admin/Menu.php:523 175 265 msgid "Security check failed." 176 266 msgstr "" 177 267 178 #: includes/Admin/Menu.php: 183179 #: includes/Admin/Menu.php: 222268 #: includes/Admin/Menu.php:322 269 #: includes/Admin/Menu.php:361 180 270 msgid "Security check failed. Please refresh the page and try again." 181 271 msgstr "" 182 272 183 #: includes/Admin/Menu.php: 208273 #: includes/Admin/Menu.php:347 184 274 msgid "Scan started." 185 275 msgstr "" 186 276 187 #: includes/Admin/Menu.php: 310277 #: includes/Admin/Menu.php:401 188 278 msgid "Scan completed." 189 279 msgstr "" 190 280 191 #: includes/Admin/Menu.php:4 34281 #: includes/Admin/Menu.php:478 192 282 msgid "Progress cleared." 193 283 msgstr "" 194 284 195 285 #. translators: %d: number of unused media found! 196 #: includes/Admin/Menu.php: 463286 #: includes/Admin/Menu.php:507 197 287 #, php-format 198 288 msgid "%d unused image found" … … 202 292 203 293 #. translators: %d: number of items deleted 204 #: includes/Admin/Menu.php:5 08294 #: includes/Admin/Menu.php:552 205 295 #, php-format 206 296 msgid "Successfully deleted %d unused media item." … … 209 299 msgstr[1] "" 210 300 211 #: includes/Admin/Menu.php:5 16301 #: includes/Admin/Menu.php:560 212 302 msgid "No unused media items found or failed to delete." 213 303 msgstr "" … … 217 307 msgstr "" 218 308 309 #: includes/Admin/Unused_Media_List.php:54 310 msgid "File" 311 msgstr "" 312 313 #: includes/Admin/Unused_Media_List.php:55 314 msgid "Author" 315 msgstr "" 316 317 #: includes/Admin/Unused_Media_List.php:56 318 #: includes/Admin/views/tabs/tab-duplicates.php:141 319 msgid "Size" 320 msgstr "" 321 219 322 #: includes/Admin/Unused_Media_List.php:57 220 msgid "File"221 msgstr ""222 223 #: includes/Admin/Unused_Media_List.php:58224 msgid "Author"225 msgstr ""226 227 #: includes/Admin/Unused_Media_List.php:59228 #: includes/Admin/views/tabs/tab-duplicates.php:137229 msgid "Size"230 msgstr ""231 232 #: includes/Admin/Unused_Media_List.php:60233 323 msgid "Date" 234 324 msgstr "" 235 325 236 #: includes/Admin/Unused_Media_List.php:8 6326 #: includes/Admin/Unused_Media_List.php:82 237 327 msgid "Edit" 238 328 msgstr "" 239 329 240 #: includes/Admin/Unused_Media_List.php:8 7241 #: includes/Admin/views/tabs/tab-overview.php:284 242 msg id "View"243 msgstr "" 244 330 #: includes/Admin/Unused_Media_List.php:84 331 msgid "Delete Permanently" 332 msgstr "" 333 334 #. translators: %s: post title 245 335 #: includes/Admin/Unused_Media_List.php:88 246 msgid "Delete Permanently"247 msgstr ""248 249 #. translators: %s: post title250 #: includes/Admin/Unused_Media_List.php:92251 336 #, php-format 252 337 msgid "\"%s\" (Edit)" 253 338 msgstr "" 254 339 255 #: includes/Admin/Unused_Media_List.php: 101340 #: includes/Admin/Unused_Media_List.php:97 256 341 msgid "File name:" 257 342 msgstr "" 258 343 259 #: includes/Admin/Unused_Media_List.php:1 111344 #: includes/Admin/Unused_Media_List.php:1055 260 345 msgid "Delete permanently" 261 346 msgstr "" 262 347 263 348 #. translators: %d: number of deleted media files 264 #: includes/Admin/Unused_Media_List.php:1 137349 #: includes/Admin/Unused_Media_List.php:1080 265 350 #, php-format 266 351 msgid "%d media file(s) deleted successfully." … … 277 362 msgstr "" 278 363 279 #: includes/Admin/views/media-tracker.php: 61364 #: includes/Admin/views/media-tracker.php:45 280 365 msgid "Add New Connection" 281 366 msgstr "" 282 367 368 #: includes/Admin/views/media-tracker.php:47 369 msgid "×" 370 msgstr "" 371 372 #: includes/Admin/views/media-tracker.php:51 373 msgid "Connection Name" 374 msgstr "" 375 376 #: includes/Admin/views/media-tracker.php:52 377 msgid "My S3 Backup" 378 msgstr "" 379 380 #: includes/Admin/views/media-tracker.php:55 381 msgid "Provider" 382 msgstr "" 383 384 #: includes/Admin/views/media-tracker.php:57 385 msgid "Google Drive" 386 msgstr "" 387 388 #: includes/Admin/views/media-tracker.php:58 389 msgid "Amazon S3" 390 msgstr "" 391 392 #: includes/Admin/views/media-tracker.php:59 393 msgid "Dropbox" 394 msgstr "" 395 283 396 #: includes/Admin/views/media-tracker.php:63 284 msgid "×" 397 msgid "Root Folder / Bucket" 398 msgstr "" 399 400 #: includes/Admin/views/media-tracker.php:64 401 msgid "/MediaTrackerPro/backup or media-tracker-pro" 285 402 msgstr "" 286 403 287 404 #: includes/Admin/views/media-tracker.php:67 288 msgid " Connection Name"405 msgid "Region / Location" 289 406 msgstr "" 290 407 291 408 #: includes/Admin/views/media-tracker.php:68 292 msgid " My S3 Backup"293 msgstr "" 294 295 #: includes/Admin/views/media-tracker.php:7 1296 msgid "Pro vider"409 msgid "us-east-1, europe-west1 etc." 410 msgstr "" 411 412 #: includes/Admin/views/media-tracker.php:70 413 msgid "Production credentials (Access Key, Secret Key) are handled via the WordPress settings page. This modal only previews UI and flow." 297 414 msgstr "" 298 415 299 416 #: includes/Admin/views/media-tracker.php:73 300 msgid " Google Drive"417 msgid "Cancel" 301 418 msgstr "" 302 419 303 420 #: includes/Admin/views/media-tracker.php:74 304 msgid " Amazon S3"421 msgid "Test Connection" 305 422 msgstr "" 306 423 307 424 #: includes/Admin/views/media-tracker.php:75 308 msgid "Dropbox"309 msgstr ""310 311 #: includes/Admin/views/media-tracker.php:79312 msgid "Root Folder / Bucket"313 msgstr ""314 315 #: includes/Admin/views/media-tracker.php:80316 msgid "/MediaTrackerPro/backup or media-tracker-pro"317 msgstr ""318 319 #: includes/Admin/views/media-tracker.php:83320 msgid "Region / Location"321 msgstr ""322 323 #: includes/Admin/views/media-tracker.php:84324 msgid "us-east-1, europe-west1 etc."325 msgstr ""326 327 #: includes/Admin/views/media-tracker.php:86328 msgid "Production credentials (Access Key, Secret Key) are handled via the WordPress settings page. This modal only previews UI and flow."329 msgstr ""330 331 #: includes/Admin/views/media-tracker.php:89332 msgid "Cancel"333 msgstr ""334 335 #: includes/Admin/views/media-tracker.php:90336 msgid "Test Connection"337 msgstr ""338 339 #: includes/Admin/views/media-tracker.php:91340 425 msgid "Save Connection" 341 msgstr ""342 343 #: includes/Admin/views/tabs/tab-documents.php:15344 #: includes/functions.php:82345 msgid "Documents"346 426 msgstr "" 347 427 … … 403 483 msgstr "" 404 484 405 #: includes/Admin/views/tabs/tab-duplicates.php:90 406 #: includes/functions.php:42 407 msgid "Duplicate Media" 408 msgstr "" 409 410 #: includes/Admin/views/tabs/tab-duplicates.php:92 485 #: includes/Admin/views/tabs/tab-duplicates.php:98 411 486 msgid "Same hash, probable duplicate images grouped together. Use delete to remove selected images." 412 487 msgstr "" 413 488 414 #: includes/Admin/views/tabs/tab-duplicates.php:12 5489 #: includes/Admin/views/tabs/tab-duplicates.php:129 415 490 msgid "Scan Duplicates" 416 491 msgstr "" 417 492 418 #: includes/Admin/views/tabs/tab-duplicates.php:13 5493 #: includes/Admin/views/tabs/tab-duplicates.php:139 419 494 msgid "Thumbnail" 420 495 msgstr "" 421 496 422 #: includes/Admin/views/tabs/tab-duplicates.php:196 423 #: includes/Admin/views/tabs/tab-overview.php:198 424 msgid "Delete" 425 msgstr "" 426 427 #: includes/Admin/views/tabs/tab-duplicates.php:204 497 #: includes/Admin/views/tabs/tab-duplicates.php:208 428 498 msgid "Delete Selected" 429 499 msgstr "" 430 500 431 #: includes/Admin/views/tabs/tab-duplicates.php:21 1501 #: includes/Admin/views/tabs/tab-duplicates.php:215 432 502 msgid "No duplicate images found." 433 503 msgstr "" 434 504 435 #: includes/Admin/views/tabs/tab-duplicates.php:21 4505 #: includes/Admin/views/tabs/tab-duplicates.php:218 436 506 msgid "Duplicate images handler not available." 437 507 msgstr "" … … 469 539 msgstr "" 470 540 541 #: includes/Admin/views/tabs/tab-license.php:16 542 msgid "License management coming soon." 543 msgstr "" 544 471 545 #: includes/Admin/views/tabs/tab-multisite.php:18 472 546 msgid "Multi-site Network Management" … … 533 607 msgstr "" 534 608 535 #: includes/Admin/views/tabs/tab-overview.php:78 536 #: includes/functions.php:26 537 msgid "Dashboard" 538 msgstr "" 539 540 #: includes/Admin/views/tabs/tab-overview.php:80 609 #: includes/Admin/views/tabs/tab-overview.php:75 541 610 msgid "Manage unused media, duplicate, optimization and cloud offload from a clean dashboard with Media Tracker." 542 msgstr ""543 544 #: includes/Admin/views/tabs/tab-overview.php:90545 #: includes/Admin/views/unused-media-list.php:21546 #: includes/functions.php:34547 msgid "Unused Media"548 611 msgstr "" 549 612 … … 551 614 #. translators: %d: Number of duplicate files. 552 615 #. translators: %d: Total number of media files. 553 #: includes/Admin/views/tabs/tab-overview.php:95 616 #: includes/Admin/views/tabs/tab-overview.php:90 617 #: includes/Admin/views/tabs/tab-overview.php:115 618 #: includes/Admin/views/tabs/tab-overview.php:132 619 #, php-format 620 msgid "%d Files" 621 msgstr "" 622 623 #. translators: %s: Formatted file size (e.g. 1.5 MB). 624 #: includes/Admin/views/tabs/tab-overview.php:98 625 #, php-format 626 msgid "Potential saving: %s" 627 msgstr "" 628 629 #: includes/Admin/views/tabs/tab-overview.php:107 630 msgid "Duplicates Found" 631 msgstr "" 632 633 #: includes/Admin/views/tabs/tab-overview.php:112 634 msgid "Scan Required" 635 msgstr "" 636 554 637 #: includes/Admin/views/tabs/tab-overview.php:120 555 #: includes/Admin/views/tabs/tab-overview.php:137556 #, php-format557 msgid "%d Files"558 msgstr ""559 560 #. translators: %s: Formatted file size (e.g. 1.5 MB).561 #: includes/Admin/views/tabs/tab-overview.php:103562 #, php-format563 msgid "Potential saving: %s"564 msgstr ""565 566 #: includes/Admin/views/tabs/tab-overview.php:112567 msgid "Duplicates Found"568 msgstr ""569 570 #: includes/Admin/views/tabs/tab-overview.php:117571 msgid "Scan Required"572 msgstr ""573 574 #: includes/Admin/views/tabs/tab-overview.php:125575 638 msgid "Based on file hash matching" 576 639 msgstr "" 577 640 578 #: includes/Admin/views/tabs/tab-overview.php:1 32641 #: includes/Admin/views/tabs/tab-overview.php:127 579 642 msgid "Total Media" 580 643 msgstr "" 581 644 582 #: includes/Admin/views/tabs/tab-overview.php:1 41645 #: includes/Admin/views/tabs/tab-overview.php:136 583 646 msgid "Total files in library" 584 647 msgstr "" 585 648 586 #: includes/Admin/views/tabs/tab-overview.php:14 9649 #: includes/Admin/views/tabs/tab-overview.php:145 587 650 msgid "Quick Actions" 588 651 msgstr "" 589 652 653 #: includes/Admin/views/tabs/tab-overview.php:150 654 msgid "Scan for Unused Media" 655 msgstr "" 656 657 #: includes/Admin/views/tabs/tab-overview.php:151 658 msgid "Scan all content to find unused media files." 659 msgstr "" 660 590 661 #: includes/Admin/views/tabs/tab-overview.php:154 591 msgid "Scan for Unused Media" 592 msgstr "" 593 594 #: includes/Admin/views/tabs/tab-overview.php:156 595 msgid "Scan all content to find unused media files." 662 msgid "Scan" 596 663 msgstr "" 597 664 598 665 #: includes/Admin/views/tabs/tab-overview.php:160 599 msgid "Scan Now"600 msgstr ""601 602 #: includes/Admin/views/tabs/tab-overview.php:175603 666 msgid "Find Duplicates" 604 667 msgstr "" 605 668 606 #: includes/Admin/views/tabs/tab-overview.php:1 77669 #: includes/Admin/views/tabs/tab-overview.php:161 607 670 msgid "Detects duplicate images using file hash matching." 608 671 msgstr "" 609 672 610 #: includes/Admin/views/tabs/tab-overview.php:1 82673 #: includes/Admin/views/tabs/tab-overview.php:164 611 674 msgid "Find" 612 675 msgstr "" 613 676 614 #: includes/Admin/views/tabs/tab-overview.php:1 88677 #: includes/Admin/views/tabs/tab-overview.php:170 615 678 msgid "Bulk Delete Unused" 616 679 msgstr "" 617 680 618 681 #. translators: %d: Number of unused files. 619 #: includes/Admin/views/tabs/tab-overview.php:1 92682 #: includes/Admin/views/tabs/tab-overview.php:174 620 683 #, php-format 621 684 msgid "%d unused files found. Delete safely after backup." 622 685 msgstr "" 623 686 624 #: includes/Admin/views/tabs/tab-overview.php:206 687 #: includes/Admin/views/tabs/tab-overview.php:179 688 msgid "Delete" 689 msgstr "" 690 691 #: includes/Admin/views/tabs/tab-overview.php:187 625 692 msgid "Media Statistics" 626 693 msgstr "" 627 694 628 695 #. translators: %d: Number of files for a specific mime type. 629 #: includes/Admin/views/tabs/tab-overview.php:2 30696 #: includes/Admin/views/tabs/tab-overview.php:209 630 697 #, php-format 631 698 msgid "%d files" 632 699 msgstr "" 633 700 634 #: includes/Admin/views/tabs/tab-overview.php:2 38701 #: includes/Admin/views/tabs/tab-overview.php:217 635 702 msgid "No media files found yet." 636 703 msgstr "" 637 704 638 #: includes/Admin/views/tabs/tab-overview.php:2 50705 #: includes/Admin/views/tabs/tab-overview.php:227 639 706 msgid "Most Used Media" 640 707 msgstr "" 641 708 642 #: includes/Admin/views/tabs/tab-overview.php:256 643 msgid "File Name" 644 msgstr "" 645 646 #: includes/Admin/views/tabs/tab-overview.php:258 647 msgid "Usage Count" 648 msgstr "" 649 650 #. translators: %d: Number of times the media is used. 651 #: includes/Admin/views/tabs/tab-overview.php:276 652 #, php-format 653 msgid "%d times" 654 msgstr "" 655 656 #: includes/Admin/views/tabs/tab-overview.php:295 657 msgid "No media usage data available yet." 709 #: includes/Admin/views/tabs/tab-overview.php:279 710 msgid "Loading usage statistics..." 658 711 msgstr "" 659 712 … … 722 775 msgstr "" 723 776 724 #: includes/functions.php:50 725 msgid "External Storage" 726 msgstr "" 727 728 #: includes/functions.php:58 729 msgid "Optimization" 730 msgstr "" 731 732 #: includes/functions.php:65 733 msgid "Security & Logs" 734 msgstr "" 735 736 #: includes/functions.php:73 737 msgid "Multi-site" 738 msgstr "" 739 740 #: includes/functions.php:90 777 #: includes/functions.php:91 741 778 msgid "Go Pro" 742 779 msgstr "" 743 780 744 781 #. translators: %s template file path 745 #: includes/functions.php:2 22782 #: includes/functions.php:255 746 783 #, php-format 747 784 msgid "%s does not exist." 748 785 msgstr "" 749 786 750 #: includes/functions.php:4 06787 #: includes/functions.php:453 751 788 msgid "This feature is available in Media Tracker Pro." 752 789 msgstr "" 753 790 754 #: includes/functions.php:4 25791 #: includes/functions.php:472 755 792 msgid "Upgrade to Media Tracker Pro" 756 793 msgstr "" 757 794 758 #: includes/Installer.php:1 81795 #: includes/Installer.php:144 759 796 msgid "Media Tracker Plugin Feedback" 760 797 msgstr "" 761 798 762 #: includes/Installer.php: 200799 #: includes/Installer.php:162 763 800 msgid "If you have a moment, we'd love to know why you're deactivating the Media Tracker plugin!" 764 801 msgstr "" 765 802 766 #: includes/Installer.php: 206803 #: includes/Installer.php:166 767 804 msgid "Enter your feedback here..." 768 805 msgstr "" 769 806 770 #: includes/Installer.php: 210807 #: includes/Installer.php:170 771 808 msgid "Skip & Deactivate" 772 809 msgstr "" 773 810 774 #: includes/Installer.php: 211811 #: includes/Installer.php:171 775 812 msgid "Submit & Deactivate" 776 813 msgstr "" -
media-tracker/trunk/media-tracker.php
r3455634 r3457950 5 5 * Author: TheBitCraft 6 6 * Author URI: https://thebitcraft.com/ 7 * Version: 1.3. 17 * Version: 1.3.2 8 8 * Requires PHP: 7.4 9 9 * Requires at least: 5.9 … … 28 28 * @var string 29 29 */ 30 const version = '1.3. 1';30 const version = '1.3.2'; 31 31 32 32 /** … … 90 90 */ 91 91 public function init_plugin() { 92 // Check for updates93 $this->check_update();94 95 92 new Media_Tracker\Media_Tracker_i18n(); 96 93 new Media_Tracker\Assets(); … … 99 96 if ( is_admin() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { 100 97 new Media_Tracker\Admin(); 101 }102 }103 104 /**105 * Check if the plugin has been updated and run the installer if necessary106 *107 * @return void108 */109 private function check_update() {110 $installed_version = get_option( 'media_tracker_version' );111 112 if ( version_compare( $installed_version, MEDIA_TRACKER_VERSION, '<' ) ) {113 $installer = new Media_Tracker\Installer();114 $installer->run();115 98 } 116 99 } -
media-tracker/trunk/readme.txt
r3455634 r3457950 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.4 8 Stable tag: 1.3. 18 Stable tag: 1.3.2 9 9 License: GPLv2 or later 10 10 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 66 66 67 67 == Changelog == 68 = 1.3.2 [10/02/2026] = 69 * Fixed: Duplicate Scan progress bar, count, and percentage now update correctly in real-time. 70 * Enhanced: Added a spinner icon to indicate active scanning state for Duplicate Scan. 71 * Fixed: "Most Used Media" section infinite loading issue optimized for better performance. 72 * New: Implemented custom menu navigation for every tab. 73 * Internal: Removed unused code and optimized backend processes. 74 68 75 = 1.3.1 [07/02/2026] = 69 76 * Fixed: Tab navigation loading issue.
Note: See TracChangeset
for help on using the changeset viewer.