Plugin Directory

Changeset 3457950


Ignore:
Timestamp:
02/10/2026 11:41:43 AM (8 days ago)
Author:
thebitcraft
Message:

Loading issue fixed

Location:
media-tracker
Files:
79 added
22 edited

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}}
    22/*# 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}
    22/*# 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">&times;</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.")})}()})});
     1jQuery(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">&times;</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  
    101101    });
    102102
    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    });
    110108
    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;
    155113        }
    156114
    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        });
    159144    });
    160 
    161145});
  • media-tracker/trunk/assets/src/js/tab.js

    r3454648 r3457950  
    7676        }
    7777    }
    78 
    79     // Tab click handlers
    80     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 params
    92             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 state
    104             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     });
    11878
    11979    // Initialize active tab from URL (only if not already set server-side)
     
    272232            }
    273233
    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...');
    275238
    276239            $('.mt-dup-wrap').show();
     
    279242            var progressBar = progressWrap.find('.mt-dup-progress-bar');
    280243            var status = $('.mt-dup-scan-status');
    281             var percent = $('.mt-dup-progress-percent');
    282244
    283245            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 
    301246            progressWrap.show();
    302247            status.show().text('Scan status: Starting... (0%)');
     
    304249            var nonce = (window.mediaTracker && window.mediaTracker.nonce) ? window.mediaTracker.nonce : '';
    305250
     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
    306257            $.post((window.mediaTracker && window.mediaTracker.ajax_url) || ajaxurl, {
    307258                action: 'reset_duplicate_hashes',
    308259                nonce: nonce
    309260            }).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
    328290                            setTimeout(function() {
    329                                 progressWrap.fadeOut(300, function() {
    330                                     location.reload();
    331                                 });
     291                                location.reload();
    332292                            }, 1000);
     293                        } else {
     294                            // Continue processing next batch
     295                            processBatch();
    333296                        }
     297                    } else {
     298                        handleError(res.data.message || 'Error processing batch.');
    334299                    }
    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            }
    361320        });
    362321    });
  • media-tracker/trunk/assets/src/scss/mt-admin.scss

    r3454648 r3457950  
    146146}
    147147
     148#usage_count {
     149    width: 140px !important;
     150}
     151
    148152.media-tracker-layout {
    149153    margin: 0;
     
    200204
    201205        li {
    202             padding: 13px 10px;
    203206            cursor: pointer;
    204207            transition: 0.25s;
     
    252255            }
    253256
    254             &.license,
    255             &.settings,
    256             &.multisite {
    257                 padding: 15px !important;
    258             }
    259 
    260             &:last-child {
    261                 padding: 0;
    262             }
    263 
    264257            &:hover {
    265258                a {
     
    787780                padding: 16px 3px;
    788781            }
     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            }
    789806        }
    790807
     
    795812            justify-content: space-between;
    796813            margin-top: 15px;
     814
     815            .tablenav {
     816                margin: 0;
     817                padding: 0;
     818            }
    797819        }
    798820
     
    801823                margin: 0 15px;
    802824            }
     825        }
     826    }
     827
     828    #tab-duplicates {
     829        #mt-dup-scan {
     830            background-color: #6366f1;
     831            color: #fff;
     832            border: none;
     833            padding: 4px 14px;
    803834        }
    804835    }
     
    831862
    832863    .mt-overview-table {
    833         border-radius: 8px;
     864        border-radius: 4px;
    834865        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);
    836866        margin-top: 20px;
    837867        width: 100%;
     
    839869        border-spacing: 0;
    840870        background: #fff;
     871        border: 1px solid #e2e8f0;
    841872
    842873        thead {
     
    854885
    855886            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; }
    858889            }
    859890        }
     
    936967    .mt-text-center { text-align: center; }
    937968    .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    }
    939975    .mt-link-clean { text-decoration: none; font-weight: 500; }
    940976
  • media-tracker/trunk/includes/Admin.php

    r3455634 r3457950  
    1818        new Admin\Duplicate_Images();
    1919        new Admin\PluginMeta();
    20         new BackgroundProcess();
    2120    }
    2221}
  • media-tracker/trunk/includes/Admin/Duplicate_Images.php

    r3454648 r3457950  
    2424        add_action('wp_ajax_get_duplicate_images', array($this, 'get_duplicate_images_via_ajax'));
    2525        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'));
    2627        add_action('wp_ajax_mt_delete_duplicate_images', array($this, 'delete_duplicate_images_via_ajax'));
    2728    }
     
    589590        delete_transient( 'media_tracker_dashboard_stats_v8' );
    590591
     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
    591596        // Trigger immediate batch processing
    592597        do_action('media_tracker_batch_process');
     
    600605                __('Reset %1$d hashes. Re-scanning %2$d images...', 'media-tracker'),
    601606                $deleted,
    602                 $remaining
     607                $total_to_scan
    603608            ),
    604609            'deleted' => $deleted,
    605             'remaining' => $remaining
     610            'remaining' => $remaining,
     611            'total' => $total_to_scan
    606612        ));
     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        }
    607650    }
    608651
     
    636679        if ( $deleted > 0 ) {
    637680            // Clear dashboard stats cache so overview updates
    638             delete_transient( 'media_tracker_dashboard_stats_v6' );
     681            delete_transient( 'media_tracker_dashboard_stats_v8' );
    639682
    640683            wp_send_json_success( array(
  • media-tracker/trunk/includes/Admin/Media_Usage.php

    r3455634 r3457950  
    2424        // Column width/styles only on Media Library list screen
    2525        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 ) );
    2689    }
    2790
    2891    /**
    2992     * Get most used media for dashboard stats.
     93     * Optimized to scan post content instead of iterating all attachments.
    3094     *
    3195     * @return array Array of objects with ID, post_title, post_mime_type, usage_count.
     
    3498        global $wpdb;
    3599
    36         $media_tracker_most_used = array();
    37100        $media_tracker_all_usage = array();
    38101
    39102        // Source 1: Featured images (_thumbnail_id).
     103        // This is efficient as it uses an index.
    40104        $media_tracker_featured_media = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    41105            "SELECT p.ID, p.post_title, p.post_mime_type, COUNT(pm.meta_value) as usage_count
     
    61125        }
    62126
    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                )
    83149            );
    84150
    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                }
    96237            }
    97238        }
    98239
    99240        // Convert to object array and sort by usage.
     241        $media_tracker_most_used = array();
    100242        foreach ( $media_tracker_all_usage as $media_tracker_usage ) {
    101243            $media_tracker_obj                 = new \stdClass();
     
    383525
    384526        $results = array();
    385         $table_name = $wpdb->prefix . 'media_tracker_index';
    386 
    387         // OPTIMIZATION: Use pre-built index table for fast lookup
    388         // This avoids expensive LIKE '%...%' queries that cause full table scans
    389         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    390         $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.NoCaching
    395             $indexed_usage = $wpdb->get_results(
    396                 $wpdb->prepare(
    397                     "SELECT DISTINCT i.source_post_id, p.post_title, p.post_type
    398                     FROM {$table_name} i
    399                     INNER JOIN {$wpdb->posts} p ON i.source_post_id = p.ID
    400                     WHERE i.media_id = %d
    401                     AND p.post_status = 'publish'",
    402                     $attachment_id
    403                 )
    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 minutes
    426                 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 exist
    432527
    433528        // Check if the image is used as site icon
     
    592687            if ( preg_match_all( '/https?:\/\/[^"\']+\/wp-content\/uploads\/[^"\']+/i', $p->post_content, $m ) ) {
    593688                foreach ( $m[0] as $url ) {
    594                     $id = \Media_Tracker\URL_Cache::get_attachment_id_from_url( $url );
     689                    $id = attachment_url_to_postid( $url );
    595690                    if ( $id && intval( $id ) === intval( $attachment_id ) ) {
    596691                        $results[] = (object) array( 'ID' => $p->ID, 'post_title' => $p->post_title, 'post_type' => $p->post_type );
     
    650745                if ( preg_match_all( '/\\b(src|url|image_url|background_image)\\s*=\\s*\"([^\"]+)\"/i', $c, $m_urls ) ) {
    651746                    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 );
    653748                        if ( $id && intval( $id ) === intval( $attachment_id ) ) {
    654749                            $found = true;
  • media-tracker/trunk/includes/Admin/Menu.php

    r3455634 r3457950  
    101101     */
    102102    public function register_media_tracker_menu() {
     103        // Main parent page - will show overview by default
    103104        add_media_page(
    104105            __( 'Media Tracker', 'media-tracker' ),
     
    108109            array( $this, 'media_tracker_admin_page' )
    109110        );
     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        }
    110195    }
    111196
    112197    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
    113252        include __DIR__ . '/views/media-tracker.php';
    114253    }
     
    236375
    237376        // Indicate scanning
    238         $progress['current_step'] = 'Scanning posts for used media...';
     377        $progress['current_step'] = 'Scanning...';
    239378        set_transient( $progress_key, $progress, 1800 );
    240379
    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
    285381        $list = new Unused_Media_List( '', null );
    286 
    287382        if ( method_exists( $list, 'force_clear_cache' ) ) {
    288383            $list->force_clear_cache();
     
    291386        // Use new method to scan and save snapshot
    292387        if ( method_exists( $list, 'scan_and_save_snapshot' ) ) {
    293             $unused_count = $list->scan_and_save_snapshot();
     388            $list->scan_and_save_snapshot();
    294389        } else {
    295390            // Fallback for safety
    296391            $list->prepare_items();
    297             $unused_count = 0;
    298         }
    299 
    300         // Update stats
    301         $scan_engine->update_stats();
     392        }
    302393
    303394        // Mark complete
     
    305396            'step' => 6,
    306397            'total_steps' => 6,
    307             'current_step' => "Scan complete. Found $unused_count unused media items."
     398            'current_step' => 'Scan complete'
    308399        ), 1800 );
    309400
     
    334425        }
    335426
     427        // Perform the scan
     428        $list = new Unused_Media_List( '', null );
     429
    336430        // Update progress state before heavy work
    337431        $progress_key = 'media_scan_progress_' . $user_id;
     
    340434            $progress = array( 'step' => 0, 'total_steps' => 6, 'current_step' => 'Starting scan...', 'used_ids' => array() );
    341435        }
    342         $progress['current_step'] = 'Scanning posts for used media...';
     436        $progress['current_step'] = 'Scanning...';
    343437        set_transient( $progress_key, $progress, 1800 );
    344 
    345         // Step 1: Clear the index table and populate it with used media using ScanEngine
    346         global $wpdb;
    347         $table_name = $wpdb->prefix . 'media_tracker_index';
    348 
    349         // Clear existing index
    350         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    351         $wpdb->query( "TRUNCATE TABLE $table_name" );
    352 
    353         // Use ScanEngine to populate the index table
    354         $scan_engine = new \Media_Tracker\ScanEngine();
    355 
    356         // Get all posts to scan
    357         $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 batches
    366         $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 periodically
    379             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 media
    386         $progress['current_step'] = 'Creating unused media snapshot...';
    387         set_transient( $progress_key, $progress, 1800 );
    388 
    389         $list = new Unused_Media_List( '', null );
    390438
    391439        if ( method_exists( $list, 'force_clear_cache' ) ) {
     
    395443        // Use new method to scan and save snapshot
    396444        if ( method_exists( $list, 'scan_and_save_snapshot' ) ) {
    397             $unused_count = $list->scan_and_save_snapshot();
     445            $list->scan_and_save_snapshot();
    398446        } else {
    399447            // Fallback for safety
    400448            $list->prepare_items();
    401             $unused_count = 0;
    402         }
    403 
    404         // Update stats
    405         $scan_engine->update_stats();
     449        }
    406450
    407451        // Mark progress as complete
     
    409453            'step' => 6,
    410454            'total_steps' => 6,
    411             'current_step' => "Scan complete. Found $unused_count unused media items."
     455            'current_step' => 'Scan complete'
    412456        ), 1800 );
    413457    }
  • media-tracker/trunk/includes/Admin/Unused_Media_List.php

    r3455634 r3457950  
    33namespace Media_Tracker\Admin;
    44
    5 defined('ABSPATH') || exit;
     5defined( 'ABSPATH' ) || exit;
    66
    77use WP_List_Table;
     
    1010 * Custom WP_List_Table implementation for managing unused media attachments.
    1111 */
    12 class Unused_Media_List extends WP_List_Table
    13 {
     12class Unused_Media_List extends WP_List_Table {
    1413
    1514    /**
     
    3332     * @param int|null $author_id Optional. Author ID to filter media by author.
    3433     */
    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(
    3836            '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;
    4442        $this->author_id = $author_id;
    4543        $this->process_bulk_action();
     
    5149     * @return array
    5250     */
    53     public function get_columns()
    54     {
     51    public function get_columns() {
    5552        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' ),
    6158        );
    6259    }
     
    6966     * @return string HTML output for the column.
    7067     */
    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 ) {
    7470            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 ) : '';
    8278                $full_file_name = $file_name . '.' . $file_extension;
    8379
    8480                // Define row actions
    8581                $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>'
    8985                ];
    9086
    9187                /* 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 );
    9389
    9490                $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 ) . '">
    9692                        <span class="media-icon image-icon">' . $thumbnail . '</span>
    97                         ' . esc_html($file_name) . '
     93                        ' . esc_html( $file_name ) . '
    9894                    </a>
    9995                </strong>
    10096                <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 ) . '
    10399                </p>';
    104100
    105101                // Append row actions
    106                 $output .= $this->row_actions($actions);
     102                $output .= $this->row_actions( $actions );
    107103                return $output;
    108104            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(
    111107                    [
    112                         'page' => 'media-tracker',
     108                        'page'   => 'media-tracker',
    113109                        'author' => $item->post_author,
    114110                    ],
    115                     admin_url('upload.php')
     111                    admin_url( 'upload.php' )
    116112                );
    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>';
    118114            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 ) );
    122118                } else {
    123119                    return '-';
    124120                }
    125121            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 ) );
    127123            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() {
    134129        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 ),
    139134        );
    140135    }
    141136
    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() {
    183143        global $wpdb;
    184144
     
    187147        // Check if we have a cached progress
    188148        $progress_key = 'media_scan_progress_' . get_current_user_id();
    189         $progress = get_transient($progress_key);
     149        $progress = get_transient( $progress_key );
    190150
    191151        // Ensure progress structure exists and is valid
    192         if (!is_array($progress)) {
     152        if ( ! is_array( $progress ) ) {
    193153            $progress = array(
    194154                'step' => 0,
     
    198158            );
    199159        } 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'] ) ) {
    204164                $progress['used_ids'] = array();
    205165            }
    206166        }
    207167
    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' ) ) {
    210170            // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Scanning can take a long time.
    211             set_time_limit(300);
     171            set_time_limit( 300 );
    212172        }
    213173
    214174        // Step 1: Featured Images
    215         if ($progress['step'] < 1) {
     175        if ( $progress['step'] < 1 ) {
    216176            $progress['current_step'] = 'Scanning featured images...';
    217             set_transient($progress_key, $progress, 300);
     177            set_transient( $progress_key, $progress, 300 );
    218178
    219179            $featured_images = $this->get_cached_db_result("
     
    224184                AND meta_value != ''
    225185            ");
    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 ) );
    228188            }
    229189            $progress['step'] = 1;
     
    231191
    232192        // Step 2: WooCommerce Images
    233         if ($progress['step'] < 2 && class_exists('WooCommerce')) {
     193        if ( $progress['step'] < 2 && class_exists( 'WooCommerce' ) ) {
    234194            $progress['current_step'] = 'Scanning WooCommerce images...';
    235             set_transient($progress_key, $progress, 300);
     195            set_transient( $progress_key, $progress, 300 );
    236196
    237197            $gallery_images = $this->get_cached_db_result("
     
    243203            ");
    244204
    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 ) ) );
    249209                }
    250210            }
     
    261221                AND meta_value != ''
    262222            ");
    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 ) );
    265225            }
    266226            $progress['step'] = 2;
     
    277237            AND meta_value != ''
    278238        ");
    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' ) ) {
    284244            $gallery_images = $this->get_cached_db_result("
    285245                SELECT DISTINCT meta_value
     
    290250            ");
    291251
    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 ) ) );
    296256                }
    297257            }
     
    308268                AND meta_value != ''
    309269            ");
    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' ) ) {
    316276            $acf_meta_values = $this->get_cached_db_result("
    317277                SELECT DISTINCT pm.meta_value
     
    328288            ");
    329289
    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 ) ) {
    332292                    continue;
    333293                }
    334294
    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 );
    337297                    continue;
    338298                }
    339299
    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 );
    346311                            }
    347312                        });
     
    350315                }
    351316
    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 );
    358328                            }
    359329                        });
     
    362332                }
    363333
    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 );
    386356                        }
    387357                    }
     
    405375            ");
    406376
    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 ) ) {
    409379                    continue;
    410380                }
    411381
    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 );
    414384                } 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 );
    421396                                }
    422397                            });
     
    427402        }
    428403
    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;
    434426        $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 ) {
    439429            $sql = $wpdb->prepare(
    440430                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     
    443433                $offset
    444434            );
    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 ) ) {
    448438                break;
    449439            }
    450440
    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] ) );
    455445                }
    456446            }
     
    461451        // Scan Gutenberg image blocks that may not include the wp-image- class
    462452        $offset_blocks = 0;
    463         while (true) {
     453        while ( true ) {
    464454            $sql_blocks = $wpdb->prepare(
    465455                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     
    468458                $offset_blocks
    469459            );
    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 ) ) {
    473463                break;
    474464            }
    475465
    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] ) );
    479469                }
    480470            }
     
    485475        // Scan other Gutenberg media blocks (Cover, Media & Text, Video, Audio, File)
    486476        $offset_blocks_ext = 0;
    487         while (true) {
     477        while ( true ) {
    488478            $sql_blocks_ext = $wpdb->prepare(
    489479                "SELECT ID, post_content FROM {$wpdb->posts} WHERE (
     
    502492                $offset_blocks_ext
    503493            );
    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 ) ) {
    507497                break;
    508498            }
    509499
    510             foreach ($posts_with_blocks_ext as $post) {
     500            foreach ( $posts_with_blocks_ext as $post ) {
    511501                // 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] ) );
    514504                }
    515505                // 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] ) );
    518508                }
    519509                // 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] ) );
    522512                }
    523513                // 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] ) );
    526516                }
    527517                // 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] ) );
    530520                }
    531521            }
     
    536526        // Scan gallery shortcodes and Gutenberg gallery blocks
    537527        $offset_gallery = 0;
    538         while (true) {
     528        while ( true ) {
    539529            $sql_gallery = $wpdb->prepare(
    540530                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     
    543533                $offset_gallery
    544534            );
    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 ) ) {
    548538                break;
    549539            }
    550540
    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 );
    565555                        }
    566556                    }
     
    573563        // Elementor: scan all posts with _elementor_data in batches and extract image IDs more accurately
    574564        $el_offset = 0;
    575         while (true) {
     565        while ( true ) {
    576566            $sql_el = $wpdb->prepare(
    577567                "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_elementor_data' LIMIT %d OFFSET %d",
     
    579569                $el_offset
    580570            );
    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 ) ) {
    584574                break;
    585575            }
    586576
    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 ) ) {
    590580                    continue;
    591581                }
    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'] );
    614595                            }
    615596                        }
    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                                }
    626612                            }
    627613                        }
    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 );
    650622            }
    651623
     
    655627        // Divi Builder: scan posts using Divi shortcodes and extract image IDs/URLs
    656628        $divi_offset = 0;
    657         while (true) {
     629        while ( true ) {
    658630            $sql_divi = $wpdb->prepare(
    659631                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d",
     
    662634                $divi_offset
    663635            );
    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 ) ) {
    667639                break;
    668640            }
    669641
    670             foreach ($divi_posts as $post) {
     642            foreach ( $divi_posts as $post ) {
    671643                $content = $post->post_content;
    672644
    673645                // 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 ) {
    678650                            $used_image_ids[] = $id;
    679651                        }
     
    682654
    683655                // 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 );
    689661                        }
    690662                    }
     
    692664
    693665                // 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 );
    699671                        }
    700672                    }
     
    707679        // Generic: scan direct uploads URLs in post content and map to attachment IDs
    708680        $offset_urls = 0;
    709         while (true) {
     681        while ( true ) {
    710682            $sql_urls = $wpdb->prepare(
    711683                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d",
     
    714686                $offset_urls
    715687            );
    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 ) ) {
    719691                break;
    720692            }
    721693
    722             foreach ($posts_with_urls as $post) {
     694            foreach ( $posts_with_urls as $post ) {
    723695                // 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 );
    729701                        }
    730702                    }
     
    737709        // Scan post_excerpt (e.g. WooCommerce Short Description)
    738710        $offset_excerpt = 0;
    739         while (true) {
     711        while ( true ) {
    740712            $sql_excerpt = $wpdb->prepare(
    741713                "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",
     
    745717                $offset_excerpt
    746718            );
    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 ) ) {
    750722                break;
    751723            }
    752724
    753             foreach ($posts_with_excerpt as $post) {
     725            foreach ( $posts_with_excerpt as $post ) {
    754726                // 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] ) );
    758730                    }
    759731                }
    760732
    761733                // 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 );
    767739                        }
    768740                    }
     
    775747        // Scan postmeta for raw URLs (custom fields, metaboxes)
    776748        $offset_meta = 0;
    777         while (true) {
     749        while ( true ) {
    778750            $sql_meta = $wpdb->prepare(
    779751                "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_value LIKE %s LIMIT %d OFFSET %d",
     
    782754                $offset_meta
    783755            );
    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 ) ) {
    787759                break;
    788760            }
    789761
    790             foreach ($meta_values as $val) {
     762            foreach ( $meta_values as $val ) {
    791763                // 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 ) {
    794766                        // 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 );
    798770                        }
    799771                    }
     
    805777        // Scan options for raw URLs (theme settings, custom options)
    806778        $offset_options = 0;
    807         while (true) {
     779        while ( true ) {
    808780            $sql_options = $wpdb->prepare(
    809781                "SELECT option_value FROM {$wpdb->options} WHERE option_value LIKE %s LIMIT %d OFFSET %d",
     
    812784                $offset_options
    813785            );
    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 ) ) {
    817789                break;
    818790            }
    819791
    820             foreach ($option_values as $val) {
     792            foreach ( $option_values as $val ) {
    821793                // 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 );
    827799                        }
    828800                    }
     
    832804        }
    833805
    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 ) {
    855827            return $id > 0 && $id < 999999999;
    856828        }));
     
    859831    }
    860832
    861     public function scan_and_save_snapshot()
    862     {
     833    public function scan_and_save_snapshot() {
    863834        global $wpdb;
    864835
    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        }
    886856
    887857        // 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' );
    898865
    899866        return count($unused_ids);
    900867    }
    901868
    902     public function prepare_items()
    903     {
     869    public function prepare_items() {
    904870        global $wpdb;
    905871
     
    907873
    908874        // 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 );
    912878        $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
    925887        $where_conditions = [
    926888            "p.post_type = 'attachment'",
    927             "p.post_status = 'inherit'",
    928             "i.media_id IS NULL"
     889            "p.post_status = 'inherit'"
    929890        ];
    930891
    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(
    984926            '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() {
    992933        // Manual mode: only refresh cache when a scan is run.
    993934        // 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 ) {
    996937            return false;
    997938        }
     
    999940        global $wpdb;
    1000941
    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(
    1004945            "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(
    1009950            "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() {
    1018958        $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 ) {
    1024963        global $wpdb;
    1025964
    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        }
    1028971
    1029972        $where_conditions = [
    1030973            "p.post_type = 'attachment'",
    1031             "p.post_status = 'inherit'",
    1032             "i.media_id IS NULL"
     974            "p.post_status = 'inherit'"
    1033975        ];
    1034976
    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 );
    1044989
    1045990        $query = "
    1046991            SELECT COUNT(*)
    1047992            FROM {$wpdb->posts} p
    1048             LEFT JOIN {$table_name} i ON p.ID = i.media_id
    1049993            $where_clause
    1050994        ";
    1051995
    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 );
    10581001    }
    10591002
     
    10641007     * @return array Array of unused media IDs.
    10651008     */
    1066     public function get_unused_media_ids($search = '')
    1067     {
     1009    public function get_unused_media_ids( $search = '' ) {
    10681010        global $wpdb;
    10691011
    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        }
    10721018
    10731019        // Build query to get all unused media
    10741020        $where_conditions = [
    10751021            "p.post_type = 'attachment'",
    1076             "p.post_status = 'inherit'",
    1077             "i.media_id IS NULL"
     1022            "p.post_status = 'inherit'"
    10781023        ];
    10791024
    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 );
    10891037
    10901038        $query = "
    10911039            SELECT p.ID
    10921040            FROM {$wpdb->posts} p
    1093             LEFT JOIN {$table_name} i ON p.ID = i.media_id
    10941041            $where_clause
    10951042        ";
    10961043
    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 ) {
    11021048        return sprintf(
    1103             '<input type="checkbox" name="media[]" value="%s" />',
    1104             $item->ID
     1049            '<input type="checkbox" name="media[]" value="%s" />', $item->ID
    11051050        );
    11061051    }
    11071052
    1108     protected function get_bulk_actions()
    1109     {
     1053    protected function get_bulk_actions() {
    11101054        return [
    1111             'delete' => __('Delete permanently', 'media-tracker'),
     1055            'delete' => __( 'Delete permanently', 'media-tracker' ),
    11121056        ];
    11131057    }
    11141058
    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() ) {
    11181061            // 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 ) {
    11251068                    // 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 );
    11281071                    }
    11291072                }
     
    11321075                $this->clear_cache();
    11331076
    1134                 $deleted_count = count($media_ids);
     1077                $deleted_count = count( $media_ids );
    11351078
    11361079                /* 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() {
    11441086        global $wpdb;
    11451087
    11461088        // 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_%'" );
    11481090        // 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' );
    11521094    }
    11531095
     
    11591101     * @return mixed Query result.
    11601102     */
    1161     private function get_cached_db_result($query, $type = 'col')
    1162     {
     1103    private function get_cached_db_result( $query, $type = 'col' ) {
    11631104        global $wpdb;
    11641105
    1165         $key = 'mt_db_' . md5($query);
     1106        $key = 'mt_db_' . md5( $query );
    11661107        $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.NotPrepared
    1172             } 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.NotPrepared
    1174             } 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.NotPrepared
    1176             }
    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 );
    11781119        }
    11791120        return $result;
    11801121    }
    11811122
    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' );
    11931132        parent::display();
    11941133    }
  • media-tracker/trunk/includes/Admin/views/media-tracker.php

    r3454648 r3457950  
    2121        <?php
    2222        $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         );
    3523
    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' );
    3834        }
    3935
    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>';
    5337        ?>
    5438
  • media-tracker/trunk/includes/Admin/views/tabs/tab-duplicates.php

    r3454648 r3457950  
    4141    $media_tracker_total_duplicate_images = count( $media_tracker_duplicate_ids_set );
    4242
     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
    4349    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verifying nonce is not required for reading sorting parameters from URL.
    4450    $media_tracker_current_sort = isset( $_GET['mt_dup_sort'] ) ? sanitize_key( wp_unslash( $_GET['mt_dup_sort'] ) ) : '';
     
    114120        echo '</div>';
    115121        echo '</div>';
    116 
    117 
    118122
    119123        echo '<form method="post" id="mt-duplicate-form">';
     
    194198            echo '<td>' . ( $media_tracker_size_display ? esc_html( $media_tracker_size_display ) : '&mdash;' ) . '</td>';
    195199            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>';
    197201            echo '</tr>';
    198202        }
  • media-tracker/trunk/includes/Admin/views/tabs/tab-overview.php

    r3455634 r3457950  
    1212 */
    1313
    14 defined('ABSPATH') || exit;
     14defined( 'ABSPATH' ) || exit;
    1515
    1616// Get real data for dashboard.
    1717
    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
     24if ( ! $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    }
    4135}
    4236
    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
     41if ( 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'];
    5557}
    5658
    57 // Get total attachments from live count for accuracy
    58 $media_tracker_count_posts = wp_count_posts('attachment');
     59// Get total attachments directly (real-time).
     60$media_tracker_count_posts       = wp_count_posts( 'attachment' );
    5961$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 );
    6263
    6364// 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();
    7368?>
    7469
     
    7671    <div class="media-header">
    7772        <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>
    7974            <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' ); ?>
    8176            </p>
    8277        </div>
     
    8883        <h3 style="display: flex; align-items: center; gap: 8px;">
    8984            <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' ); ?>
    9186        </h3>
    9287        <span class="value">
    9388            <?php
    9489            /* 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                 <?php
    102                 /* 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>
    106101        <?php endif; ?>
    107102    </div>
     
    110105        <h3 style="display: flex; align-items: center; gap: 8px;">
    111106            <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' ); ?>
    113108        </h3>
    114109        <span class="value">
    115110            <?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>';
    118113            } else {
    119114                /* 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 ) );
    121116            }
    122117            ?>
    123118        </span>
    124119        <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' ); ?>
    126121        </span>
    127122    </div>
     
    130125        <h3 style="display: flex; align-items: center; gap: 8px;">
    131126            <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' ); ?>
    133128        </h3>
    134129        <span class="value">
    135130            <?php
    136131            /* 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>
    142138    </div>
    143139</div>
     
    147143        <div class="section-title">
    148144            <i class="dashicons dashicons-superhero"></i>
    149             <?php esc_html_e('Quick Actions', 'media-tracker'); ?>
     145            <?php esc_html_e( 'Quick Actions', 'media-tracker' ); ?>
    150146        </div>
    151147
    152148        <div class="setting-item" style="margin-top: 10px;">
    153149            <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>
    158152            </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' ); ?>
    161155            </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>
    171156        </div>
    172157
    173158        <div class="setting-item">
    174159            <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>
    179162            </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' ); ?>
    183165            </button>
    184166        </div>
     
    186168        <div class="setting-item">
    187169            <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>
    189171                <p style="font-size: 12px; color: #64748b;">
    190172                    <?php
    191173                    /* 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 ) );
    193175                    ?>
    194176                </p>
    195177            </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' ); ?>
    199180            </button>
    200181        </div>
     
    204185        <div class="section-title">
    205186            <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' ); ?>
    207188        </div>
    208189        <div class="stacked">
     
    210191            // Get media type statistics.
    211192            $media_tracker_mime_types = array();
    212             if (class_exists('\Media_Tracker\Admin\Media_Usage')) {
     193            if ( class_exists( '\Media_Tracker\Admin\Media_Usage' ) ) {
    213194                $media_tracker_mime_types = \Media_Tracker\Admin\Media_Usage::get_mime_type_stats();
    214195            }
    215196            ?>
    216197
    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>
    223202                        <div style="flex: 1;">
    224203                            <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 ) ); ?>
    226205                            </div>
    227206                            <div class="sub-label">
    228207                                <?php
    229208                                /* 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 ) );
    231210                                ?>
    232211                            </div>
     
    234213                    </div>
    235214                <?php endforeach; ?>
    236             <?php else: ?>
     215            <?php else : ?>
    237216                <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' ); ?>
    239218                </p>
    240219            <?php endif; ?>
     
    242221    </div>
    243222</div>
    244 
    245 
    246223
    247224<div class="card" style="margin-top: 1.5rem;">
    248225    <div class="section-title">
    249226        <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>
    298304</div>
  • media-tracker/trunk/includes/Admin/views/unused-media-list.php

    r3454648 r3457950  
    7272    var clientProgress = 0; // Client-side progress simulation
    7373    var targetProgress = 0; // Target progress from server
     74    var currentStepName = 'Starting...';
    7475    var progressAnimInterval = null; // Animation interval
    7576
     
    147148    // Smooth progress animation function
    148149    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
    149156        // Gradually move clientProgress towards targetProgress
    150157        if (clientProgress < targetProgress) {
    151158            // Increase gradually based on distance
    152159            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);
    154162
    155163            clientProgress += increment;
     
    158166            }
    159167            $('#media-scan-progress-fill').css('width', clientProgress + '%');
     168            $('#media-scan-progress-text').html('Scan status: ' + currentStepName + ' (' + Math.floor(clientProgress) + '%)');
    160169        }
    161170    }
     
    173182            var d = res.data;
    174183            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            }
    178196
    179197            // Fallback: if scan appears stalled at start, trigger synchronous scan
     
    199217                targetProgress = 100;
    200218                $('#media-scan-progress-fill').css('width', '100%');
     219
     220                // Remove tab closing prevention
     221                $(window).off('beforeunload.mediaTrackerScan');
    201222
    202223                // Clear progress transient to avoid sticky progress UI
     
    263284        clientProgress = 0;
    264285        targetProgress = 5; // Start with 5%
     286        currentStepName = 'Starting...';
    265287        syncTriggered = false;
    266288        stuckChecks = 0;
     
    272294        $('#media-scan-progress-fill').css('width', '0%');
    273295        $('#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        });
    275302
    276303        $.post(AJAX_URL, {
     
    278305            nonce: NONCE
    279306        }).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...');
    281308
    282309            // Start smooth animation immediately
     
    292319            }, 3000);
    293320            ensureScanButtonExists('Scan Unused Media').prop('disabled', false);
     321
     322            // Remove tab closing prevention
     323            $(window).off('beforeunload.mediaTrackerScan');
    294324        });
    295325
  • media-tracker/trunk/includes/Assets.php

    r3454648 r3457950  
    4242        );
    4343
    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 ) {
    4646            $data['base_url'] = admin_url( 'upload.php?page=media-tracker' );
    4747
  • media-tracker/trunk/includes/Installer.php

    r3455634 r3457950  
    33namespace Media_Tracker;
    44
    5 defined('ABSPATH') || exit;
     5defined( 'ABSPATH' ) || exit;
    66
    77/**
    88 * Installer class
    99 */
    10 class Installer
    11 {
     10class Installer {
    1211
    1312    /**
     
    1918     * @return  void
    2019     */
    21     public function run()
    22     {
     20    public function run() {
    2321        $this->add_version();
    24         $this->create_index_table();
    2522        $this->optimize_database_indexes();
    2623        $this->schedule_cron_jobs();
    27     }
    28 
    29     /**
    30      * Create the custom index table for media relationships
    31      *
    32      * @since 1.4.0
    33      */
    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);
    5524    }
    5625
     
    6332     * @return  void
    6433     */
    65     public function schedule_cron_jobs()
    66     {
     34    public function schedule_cron_jobs() {
    6735        // Schedule the batch processing cron job
    6836        if (!wp_next_scheduled('media_tracker_batch_process')) {
     
    8553     * @return  void
    8654     */
    87     public static function clear_cron_jobs()
    88     {
     55    public static function clear_cron_jobs() {
    8956        // Clear the batch processing cron job
    9057        $timestamp = wp_next_scheduled('media_tracker_batch_process');
     
    10572     * @return  void
    10673     */
    107     public function optimize_database_indexes()
    108     {
     74    public function optimize_database_indexes() {
    10975        global $wpdb;
    11076
    11177        // 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 );
    11379
    114         if (!$indexes_created) {
     80        if ( ! $indexes_created ) {
    11581            // MySQL-compatible indexes (no partial WHERE clauses)
    11682            // Speed up duplicate hash aggregation: index on meta_key and meta_value prefix
     
    11985
    12086            // 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.NoCaching
    122             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 ) ) {
    12389                // 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.SchemaChange
     90                $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
    12591            }
    12692
    12793            // 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.NoCaching
    129             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 ) ) {
    13096                // 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.SchemaChange
     97                $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
    13298            }
    13399
    134100            // New: index to accelerate attachment/mime filtering used by duplicate queries
    135101            // 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.NoCaching
    137             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 ) ) {
    138104                // 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.SchemaChange
     105                $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
    140106            }
    141107
    142             update_option('media_tracker_indexes_created', true);
     108            update_option( 'media_tracker_indexes_created', true );
    143109        }
    144110    }
     
    152118     * @return  void
    153119     */
    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' );
    157122
    158         if (!$installed) {
    159             update_option('media_tracker_installed', time());
     123        if ( ! $installed ) {
     124            update_option( 'media_tracker_installed', time() );
    160125        }
    161126
    162         update_option('media_tracker_version', MEDIA_TRACKER_VERSION);
     127        update_option( 'media_tracker_version', MEDIA_TRACKER_VERSION );
    163128    }
    164129
    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' ) );
    168132    }
    169133
     
    171135     * AJAX handler to save feedback
    172136     */
    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' );
    176139
    177140        $feedback = isset($_POST['feedback']) ? sanitize_textarea_field(wp_unslash($_POST['feedback'])) : '';
    178141
    179         if (!empty($feedback)) {
     142        if ( ! empty( $feedback ) ) {
    180143            $to = '[email protected]';
    181             $subject = __('Media Tracker Plugin Feedback', 'media-tracker');
     144            $subject = __( 'Media Tracker Plugin Feedback', 'media-tracker' );
    182145            $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' );
    184147
    185             wp_mail($to, $subject, $message, $headers);
     148            wp_mail( $to, $subject, $message, $headers );
    186149
    187150            wp_send_json_success();
     
    192155     * Output HTML for feedback modal
    193156     */
    194     public static function feedback_modal_html()
    195     { ?>
     157    public static function feedback_modal_html() { ?>
    196158        <div id="mt-feedback-modal">
    197159            <div class="mt-feedback-modal-content">
    198160                <header class="mt-feedback-modal-header">
    199161                    <span class="close">&times;</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>
    202163                </header>
    203164
    204165                <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>
    207167                </div>
    208168
    209169                <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>
    212172                </footer>
    213173            </div>
  • media-tracker/trunk/includes/functions.php

    r3454648 r3457950  
    2727            'icon'    => 'dashicons dashicons-admin-home',
    2828            'badge'   => '',
    29             'is_link' => false,
    30             'url'     => '',
     29            'is_link' => true,
     30            'url'     => admin_url( 'upload.php?page=media-tracker-overview' ),
    3131            'active'  => false,
    3232        ),
     
    3535            'icon'    => 'dashicons dashicons-format-image',
    3636            'badge'   => '',
    37             'is_link' => false,
    38             'url'     => '',
     37            'is_link' => true,
     38            'url'     => admin_url( 'upload.php?page=media-tracker-unused-media' ),
    3939            'active'  => false,
    4040        ),
     
    4343            'icon'    => 'dashicons dashicons-images-alt',
    4444            'badge'   => '',
    45             'is_link' => false,
    46             'url'     => '',
     45            'is_link' => true,
     46            'url'     => admin_url( 'upload.php?page=media-tracker-duplicates' ),
    4747            'active'  => false,
    4848        ),
     
    5151            'icon'    => 'dashicons dashicons-cloud-upload',
    5252            'badge'   => '',
    53             'is_link' => false,
    54             'url'     => '',
     53            'is_link' => true,
     54            'url'     => admin_url( 'upload.php?page=media-tracker-external-storage' ),
    5555            'active'  => false,
    5656        ),
     
    5858            'label'   => __( 'Optimization', 'media-tracker' ),
    5959            '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' ),
    6263            'active'  => false,
    6364        ),
     
    6667            'icon'    => 'dashicons dashicons-lock',
    6768            'badge'   => '',
    68             'is_link' => false,
    69             'url'     => '',
     69            'is_link' => true,
     70            'url'     => admin_url( 'upload.php?page=media-tracker-security' ),
    7071            'active'  => false,
    7172        ),
     
    7475            'icon'    => 'dashicons dashicons-admin-multisite',
    7576            'badge'   => '',
    76             'is_link' => false,
    77             'url'     => '',
     77            'is_link' => true,
     78            'url'     => admin_url( 'upload.php?page=media-tracker-multisite' ),
    7879            'active'  => false,
    7980            'class'   => 'multisite'
     
    8384            'icon'    => 'dashicons dashicons-media-document',
    8485            'badge'   => '',
    85             'is_link' => false,
    86             'url'     => '',
     86            'is_link' => true,
     87            'url'     => admin_url( 'upload.php?page=media-tracker-documents' ),
    8788            'active'  => false,
    8889        ),
     
    9798        ),
    9899    );
     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    }
    99112
    100113    // Set active state for current tab
     
    113126
    114127/**
    115  * Get current active tab from URL parameter
     128 * Get current active tab from URL parameter or page parameter
    116129 *
    117130 * @since 1.3.0
     
    119132 */
    120133function media_tracker_get_current_tab() {
    121     // Check URL parameter 'tab'
     134    $tab = '';
     135
     136    // First check URL parameter 'tab' (for backward compatibility)
    122137    // 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)
    126159    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' );
    128161        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required for reading the query string.
    129162        $query_string = isset( $_SERVER['QUERY_STRING'] ) ? sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : '';
     
    299332
    300333    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 ) ) . '"' : '';
    303349        $target_attr  = ! empty( $item['target'] ) ? ' target="' . esc_attr( $item['target'] ) . '"' : '';
    304350
    305351        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.
    307354            esc_url( $item['url'] ),
    308355            $custom_class, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Class attribute is constructed with esc_attr.
     
    313360        );
    314361    } else {
    315         // For regular tabs
     362        // For regular tabs (non-link items, if any)
    316363        $classes = array();
    317364
  • media-tracker/trunk/languages/media-tracker.pot

    r3455634 r3457950  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Media Tracker 1.3.0\n"
     5"Project-Id-Version: Media Tracker 1.3.2\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/media-tracker\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"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"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
     
    1717#. Plugin Name of the plugin
    1818#: media-tracker.php
    19 #: includes/Admin/Menu.php:104
    2019#: includes/Admin/Menu.php:105
     20#: includes/Admin/Menu.php:106
    2121msgid "Media Tracker"
    2222msgstr ""
     
    3737msgstr ""
    3838
    39 #: includes/Admin/Duplicate_Images.php:43
     39#: includes/Admin/Duplicate_Images.php:44
    4040msgid "Duplicates"
    4141msgstr ""
    4242
    43 #: includes/Admin/Duplicate_Images.php:45
     43#: includes/Admin/Duplicate_Images.php:46
    4444msgid "Duplicate images filter"
    4545msgstr ""
    4646
    47 #: includes/Admin/Duplicate_Images.php:46
     47#: includes/Admin/Duplicate_Images.php:47
    4848msgid "All Media"
    4949msgstr ""
    5050
    51 #: includes/Admin/Duplicate_Images.php:48
     51#: includes/Admin/Duplicate_Images.php:49
    5252msgid "Show Duplicate Images"
    5353msgstr ""
    5454
    55 #: includes/Admin/Duplicate_Images.php:53
     55#: includes/Admin/Duplicate_Images.php:54
    5656msgid "Re-scan"
    5757msgstr ""
    5858
    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
    6364msgid "Unauthorized"
    6465msgstr ""
    6566
    6667#. translators: 1: number of hashes reset, 2: number of images being rescanned
    67 #: includes/Admin/Duplicate_Images.php:600
     68#: includes/Admin/Duplicate_Images.php:605
    6869#, php-format
    6970msgid "Reset %1$d hashes. Re-scanning %2$d images..."
    7071msgstr ""
    7172
    72 #: includes/Admin/Duplicate_Images.php:617
     73#: includes/Admin/Duplicate_Images.php:660
    7374msgid "No images selected."
    7475msgstr ""
    7576
    7677#. translators: %d: number of deleted images
    77 #: includes/Admin/Duplicate_Images.php:643
     78#: includes/Admin/Duplicate_Images.php:686
    7879#, php-format
    7980msgid "Deleted %d duplicate images."
    8081msgstr ""
    8182
    82 #: includes/Admin/Duplicate_Images.php:649
     83#: includes/Admin/Duplicate_Images.php:692
    8384msgid "No images were deleted."
    8485msgstr ""
    8586
    86 #: includes/Admin/Duplicate_Images.php:684
     87#: includes/Admin/Duplicate_Images.php:727
    8788msgid "items"
    8889msgstr ""
    8990
    90 #: includes/Admin/Media_Usage.php:153
     91#: includes/Admin/Media_Usage.php:47
     92#: includes/Admin/views/tabs/tab-overview.php:236
     93msgid "File Name"
     94msgstr ""
     95
     96#: includes/Admin/Media_Usage.php:48
     97#: includes/Admin/Media_Usage.php:433
     98#: includes/Admin/views/tabs/tab-overview.php:237
     99msgid "Type"
     100msgstr ""
     101
     102#: includes/Admin/Media_Usage.php:49
     103#: includes/Admin/views/tabs/tab-overview.php:238
     104msgid "Usage Count"
     105msgstr ""
     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
     111msgid "Actions"
     112msgstr ""
     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
     118msgid "%d times"
     119msgstr ""
     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
     124msgid "View"
     125msgstr ""
     126
     127#: includes/Admin/Media_Usage.php:84
     128#: includes/Admin/views/tabs/tab-overview.php:273
     129msgid "No media usage data available."
     130msgstr ""
     131
     132#: includes/Admin/Media_Usage.php:295
    91133msgid "Media Usage"
    92134msgstr ""
    93135
    94 #: includes/Admin/Media_Usage.php:168
    95 #: includes/Admin/views/tabs/tab-duplicates.php:151
     136#: includes/Admin/Media_Usage.php:310
     137#: includes/Admin/views/tabs/tab-duplicates.php:155
    96138msgid "Usages Count"
    97139msgstr ""
    98140
    99 #: includes/Admin/Media_Usage.php:190
     141#: includes/Admin/Media_Usage.php:332
    100142msgid "Open attachment edit screen"
    101143msgstr ""
    102144
    103 #: includes/Admin/Media_Usage.php:289
     145#: includes/Admin/Media_Usage.php:431
    104146msgid "#"
    105147msgstr ""
    106148
    107 #: includes/Admin/Media_Usage.php:290
    108 #: includes/Admin/views/tabs/tab-duplicates.php:136
     149#: includes/Admin/Media_Usage.php:432
     150#: includes/Admin/views/tabs/tab-duplicates.php:140
    109151msgid "Title"
    110152msgstr ""
    111153
    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
    118155msgid "Date Added"
    119156msgstr ""
    120157
    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
    128159msgid "System Setting"
    129160msgstr ""
    130161
    131 #: includes/Admin/Media_Usage.php:309
     162#: includes/Admin/Media_Usage.php:451
    132163msgid "Customize Site Icon"
    133164msgstr ""
    134165
    135166#. Translators: This is a time difference string
    136 #: includes/Admin/Media_Usage.php:319
     167#: includes/Admin/Media_Usage.php:461
    137168#, php-format
    138169msgid "%s ago"
    139170msgstr ""
    140171
    141 #: includes/Admin/Media_Usage.php:341
     172#: includes/Admin/Media_Usage.php:483
    142173msgid "Admin View"
    143174msgstr ""
    144175
    145 #: includes/Admin/Media_Usage.php:342
     176#: includes/Admin/Media_Usage.php:484
    146177msgid "Frontend View"
    147178msgstr ""
    148179
    149 #: includes/Admin/Media_Usage.php:350
     180#: includes/Admin/Media_Usage.php:492
    150181msgid "No posts or pages found using this media file."
    151182msgstr ""
    152183
    153 #: includes/Admin/Media_Usage.php:420
    154 #: includes/Admin/Media_Usage.php:438
     184#: includes/Admin/Media_Usage.php:533
    155185msgid "Site Icon (Favicon)"
    156186msgstr ""
    157187
    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
     192msgid "Dashboard"
     193msgstr ""
     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
     200msgid "Unused Media"
     201msgstr ""
     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
     207msgid "Duplicate Media"
     208msgstr ""
     209
     210#: includes/Admin/Menu.php:142
     211#: includes/Admin/Menu.php:143
     212#: includes/functions.php:50
     213msgid "External Storage"
     214msgstr ""
     215
     216#: includes/Admin/Menu.php:151
     217#: includes/Admin/Menu.php:152
     218#: includes/functions.php:58
     219msgid "Optimization"
     220msgstr ""
     221
     222#: includes/Admin/Menu.php:160
     223#: includes/Admin/Menu.php:161
     224#: includes/functions.php:66
     225msgid "Security & Logs"
     226msgstr ""
     227
     228#: includes/Admin/Menu.php:169
     229#: includes/Admin/Menu.php:170
     230#: includes/functions.php:74
     231msgid "Multi-site"
     232msgstr ""
     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
     238msgid "Documents"
     239msgstr ""
     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
     245msgid "License"
     246msgstr ""
     247
     248#: includes/Admin/Menu.php:268
    159249msgid "Cache cleared successfully."
    160250msgstr ""
    161251
    162 #: includes/Admin/Menu.php:137
    163 #: includes/Admin/Menu.php:178
    164 #: includes/Admin/Menu.php:217
    165 #: includes/Admin/Menu.php:420
    166 #: includes/Admin/Menu.php:442
    167 #: includes/Admin/Menu.php:474
     252#: 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
    168258msgid "Unauthorized: You do not have permission to perform this action."
    169259msgstr ""
    170260
    171 #: includes/Admin/Menu.php:142
    172 #: includes/Admin/Menu.php:425
    173 #: includes/Admin/Menu.php:447
    174 #: includes/Admin/Menu.php:479
     261#: includes/Admin/Menu.php:281
     262#: includes/Admin/Menu.php:469
     263#: includes/Admin/Menu.php:491
     264#: includes/Admin/Menu.php:523
    175265msgid "Security check failed."
    176266msgstr ""
    177267
    178 #: includes/Admin/Menu.php:183
    179 #: includes/Admin/Menu.php:222
     268#: includes/Admin/Menu.php:322
     269#: includes/Admin/Menu.php:361
    180270msgid "Security check failed. Please refresh the page and try again."
    181271msgstr ""
    182272
    183 #: includes/Admin/Menu.php:208
     273#: includes/Admin/Menu.php:347
    184274msgid "Scan started."
    185275msgstr ""
    186276
    187 #: includes/Admin/Menu.php:310
     277#: includes/Admin/Menu.php:401
    188278msgid "Scan completed."
    189279msgstr ""
    190280
    191 #: includes/Admin/Menu.php:434
     281#: includes/Admin/Menu.php:478
    192282msgid "Progress cleared."
    193283msgstr ""
    194284
    195285#. translators: %d: number of unused media found!
    196 #: includes/Admin/Menu.php:463
     286#: includes/Admin/Menu.php:507
    197287#, php-format
    198288msgid "%d unused image found"
     
    202292
    203293#. translators: %d: number of items deleted
    204 #: includes/Admin/Menu.php:508
     294#: includes/Admin/Menu.php:552
    205295#, php-format
    206296msgid "Successfully deleted %d unused media item."
     
    209299msgstr[1] ""
    210300
    211 #: includes/Admin/Menu.php:516
     301#: includes/Admin/Menu.php:560
    212302msgid "No unused media items found or failed to delete."
    213303msgstr ""
     
    217307msgstr ""
    218308
     309#: includes/Admin/Unused_Media_List.php:54
     310msgid "File"
     311msgstr ""
     312
     313#: includes/Admin/Unused_Media_List.php:55
     314msgid "Author"
     315msgstr ""
     316
     317#: includes/Admin/Unused_Media_List.php:56
     318#: includes/Admin/views/tabs/tab-duplicates.php:141
     319msgid "Size"
     320msgstr ""
     321
    219322#: includes/Admin/Unused_Media_List.php:57
    220 msgid "File"
    221 msgstr ""
    222 
    223 #: includes/Admin/Unused_Media_List.php:58
    224 msgid "Author"
    225 msgstr ""
    226 
    227 #: includes/Admin/Unused_Media_List.php:59
    228 #: includes/Admin/views/tabs/tab-duplicates.php:137
    229 msgid "Size"
    230 msgstr ""
    231 
    232 #: includes/Admin/Unused_Media_List.php:60
    233323msgid "Date"
    234324msgstr ""
    235325
    236 #: includes/Admin/Unused_Media_List.php:86
     326#: includes/Admin/Unused_Media_List.php:82
    237327msgid "Edit"
    238328msgstr ""
    239329
    240 #: includes/Admin/Unused_Media_List.php:87
    241 #: includes/Admin/views/tabs/tab-overview.php:284
    242 msgid "View"
    243 msgstr ""
    244 
     330#: includes/Admin/Unused_Media_List.php:84
     331msgid "Delete Permanently"
     332msgstr ""
     333
     334#. translators: %s: post title
    245335#: includes/Admin/Unused_Media_List.php:88
    246 msgid "Delete Permanently"
    247 msgstr ""
    248 
    249 #. translators: %s: post title
    250 #: includes/Admin/Unused_Media_List.php:92
    251336#, php-format
    252337msgid "\"%s\" (Edit)"
    253338msgstr ""
    254339
    255 #: includes/Admin/Unused_Media_List.php:101
     340#: includes/Admin/Unused_Media_List.php:97
    256341msgid "File name:"
    257342msgstr ""
    258343
    259 #: includes/Admin/Unused_Media_List.php:1111
     344#: includes/Admin/Unused_Media_List.php:1055
    260345msgid "Delete permanently"
    261346msgstr ""
    262347
    263348#. translators: %d: number of deleted media files
    264 #: includes/Admin/Unused_Media_List.php:1137
     349#: includes/Admin/Unused_Media_List.php:1080
    265350#, php-format
    266351msgid "%d media file(s) deleted successfully."
     
    277362msgstr ""
    278363
    279 #: includes/Admin/views/media-tracker.php:61
     364#: includes/Admin/views/media-tracker.php:45
    280365msgid "Add New Connection"
    281366msgstr ""
    282367
     368#: includes/Admin/views/media-tracker.php:47
     369msgid "&times;"
     370msgstr ""
     371
     372#: includes/Admin/views/media-tracker.php:51
     373msgid "Connection Name"
     374msgstr ""
     375
     376#: includes/Admin/views/media-tracker.php:52
     377msgid "My S3 Backup"
     378msgstr ""
     379
     380#: includes/Admin/views/media-tracker.php:55
     381msgid "Provider"
     382msgstr ""
     383
     384#: includes/Admin/views/media-tracker.php:57
     385msgid "Google Drive"
     386msgstr ""
     387
     388#: includes/Admin/views/media-tracker.php:58
     389msgid "Amazon S3"
     390msgstr ""
     391
     392#: includes/Admin/views/media-tracker.php:59
     393msgid "Dropbox"
     394msgstr ""
     395
    283396#: includes/Admin/views/media-tracker.php:63
    284 msgid "&times;"
     397msgid "Root Folder / Bucket"
     398msgstr ""
     399
     400#: includes/Admin/views/media-tracker.php:64
     401msgid "/MediaTrackerPro/backup or media-tracker-pro"
    285402msgstr ""
    286403
    287404#: includes/Admin/views/media-tracker.php:67
    288 msgid "Connection Name"
     405msgid "Region / Location"
    289406msgstr ""
    290407
    291408#: includes/Admin/views/media-tracker.php:68
    292 msgid "My S3 Backup"
    293 msgstr ""
    294 
    295 #: includes/Admin/views/media-tracker.php:71
    296 msgid "Provider"
     409msgid "us-east-1, europe-west1 etc."
     410msgstr ""
     411
     412#: includes/Admin/views/media-tracker.php:70
     413msgid "Production credentials (Access Key, Secret Key) are handled via the WordPress settings page. This modal only previews UI and flow."
    297414msgstr ""
    298415
    299416#: includes/Admin/views/media-tracker.php:73
    300 msgid "Google Drive"
     417msgid "Cancel"
    301418msgstr ""
    302419
    303420#: includes/Admin/views/media-tracker.php:74
    304 msgid "Amazon S3"
     421msgid "Test Connection"
    305422msgstr ""
    306423
    307424#: includes/Admin/views/media-tracker.php:75
    308 msgid "Dropbox"
    309 msgstr ""
    310 
    311 #: includes/Admin/views/media-tracker.php:79
    312 msgid "Root Folder / Bucket"
    313 msgstr ""
    314 
    315 #: includes/Admin/views/media-tracker.php:80
    316 msgid "/MediaTrackerPro/backup or media-tracker-pro"
    317 msgstr ""
    318 
    319 #: includes/Admin/views/media-tracker.php:83
    320 msgid "Region / Location"
    321 msgstr ""
    322 
    323 #: includes/Admin/views/media-tracker.php:84
    324 msgid "us-east-1, europe-west1 etc."
    325 msgstr ""
    326 
    327 #: includes/Admin/views/media-tracker.php:86
    328 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:89
    332 msgid "Cancel"
    333 msgstr ""
    334 
    335 #: includes/Admin/views/media-tracker.php:90
    336 msgid "Test Connection"
    337 msgstr ""
    338 
    339 #: includes/Admin/views/media-tracker.php:91
    340425msgid "Save Connection"
    341 msgstr ""
    342 
    343 #: includes/Admin/views/tabs/tab-documents.php:15
    344 #: includes/functions.php:82
    345 msgid "Documents"
    346426msgstr ""
    347427
     
    403483msgstr ""
    404484
    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
    411486msgid "Same hash, probable duplicate images grouped together. Use delete to remove selected images."
    412487msgstr ""
    413488
    414 #: includes/Admin/views/tabs/tab-duplicates.php:125
     489#: includes/Admin/views/tabs/tab-duplicates.php:129
    415490msgid "Scan Duplicates"
    416491msgstr ""
    417492
    418 #: includes/Admin/views/tabs/tab-duplicates.php:135
     493#: includes/Admin/views/tabs/tab-duplicates.php:139
    419494msgid "Thumbnail"
    420495msgstr ""
    421496
    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
    428498msgid "Delete Selected"
    429499msgstr ""
    430500
    431 #: includes/Admin/views/tabs/tab-duplicates.php:211
     501#: includes/Admin/views/tabs/tab-duplicates.php:215
    432502msgid "No duplicate images found."
    433503msgstr ""
    434504
    435 #: includes/Admin/views/tabs/tab-duplicates.php:214
     505#: includes/Admin/views/tabs/tab-duplicates.php:218
    436506msgid "Duplicate images handler not available."
    437507msgstr ""
     
    469539msgstr ""
    470540
     541#: includes/Admin/views/tabs/tab-license.php:16
     542msgid "License management coming soon."
     543msgstr ""
     544
    471545#: includes/Admin/views/tabs/tab-multisite.php:18
    472546msgid "Multi-site Network Management"
     
    533607msgstr ""
    534608
    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
    541610msgid "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:90
    545 #: includes/Admin/views/unused-media-list.php:21
    546 #: includes/functions.php:34
    547 msgid "Unused Media"
    548611msgstr ""
    549612
     
    551614#. translators: %d: Number of duplicate files.
    552615#. 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
     620msgid "%d Files"
     621msgstr ""
     622
     623#. translators: %s: Formatted file size (e.g. 1.5 MB).
     624#: includes/Admin/views/tabs/tab-overview.php:98
     625#, php-format
     626msgid "Potential saving: %s"
     627msgstr ""
     628
     629#: includes/Admin/views/tabs/tab-overview.php:107
     630msgid "Duplicates Found"
     631msgstr ""
     632
     633#: includes/Admin/views/tabs/tab-overview.php:112
     634msgid "Scan Required"
     635msgstr ""
     636
    554637#: includes/Admin/views/tabs/tab-overview.php:120
    555 #: includes/Admin/views/tabs/tab-overview.php:137
    556 #, php-format
    557 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:103
    562 #, php-format
    563 msgid "Potential saving: %s"
    564 msgstr ""
    565 
    566 #: includes/Admin/views/tabs/tab-overview.php:112
    567 msgid "Duplicates Found"
    568 msgstr ""
    569 
    570 #: includes/Admin/views/tabs/tab-overview.php:117
    571 msgid "Scan Required"
    572 msgstr ""
    573 
    574 #: includes/Admin/views/tabs/tab-overview.php:125
    575638msgid "Based on file hash matching"
    576639msgstr ""
    577640
    578 #: includes/Admin/views/tabs/tab-overview.php:132
     641#: includes/Admin/views/tabs/tab-overview.php:127
    579642msgid "Total Media"
    580643msgstr ""
    581644
    582 #: includes/Admin/views/tabs/tab-overview.php:141
     645#: includes/Admin/views/tabs/tab-overview.php:136
    583646msgid "Total files in library"
    584647msgstr ""
    585648
    586 #: includes/Admin/views/tabs/tab-overview.php:149
     649#: includes/Admin/views/tabs/tab-overview.php:145
    587650msgid "Quick Actions"
    588651msgstr ""
    589652
     653#: includes/Admin/views/tabs/tab-overview.php:150
     654msgid "Scan for Unused Media"
     655msgstr ""
     656
     657#: includes/Admin/views/tabs/tab-overview.php:151
     658msgid "Scan all content to find unused media files."
     659msgstr ""
     660
    590661#: 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."
     662msgid "Scan"
    596663msgstr ""
    597664
    598665#: includes/Admin/views/tabs/tab-overview.php:160
    599 msgid "Scan Now"
    600 msgstr ""
    601 
    602 #: includes/Admin/views/tabs/tab-overview.php:175
    603666msgid "Find Duplicates"
    604667msgstr ""
    605668
    606 #: includes/Admin/views/tabs/tab-overview.php:177
     669#: includes/Admin/views/tabs/tab-overview.php:161
    607670msgid "Detects duplicate images using file hash matching."
    608671msgstr ""
    609672
    610 #: includes/Admin/views/tabs/tab-overview.php:182
     673#: includes/Admin/views/tabs/tab-overview.php:164
    611674msgid "Find"
    612675msgstr ""
    613676
    614 #: includes/Admin/views/tabs/tab-overview.php:188
     677#: includes/Admin/views/tabs/tab-overview.php:170
    615678msgid "Bulk Delete Unused"
    616679msgstr ""
    617680
    618681#. translators: %d: Number of unused files.
    619 #: includes/Admin/views/tabs/tab-overview.php:192
     682#: includes/Admin/views/tabs/tab-overview.php:174
    620683#, php-format
    621684msgid "%d unused files found. Delete safely after backup."
    622685msgstr ""
    623686
    624 #: includes/Admin/views/tabs/tab-overview.php:206
     687#: includes/Admin/views/tabs/tab-overview.php:179
     688msgid "Delete"
     689msgstr ""
     690
     691#: includes/Admin/views/tabs/tab-overview.php:187
    625692msgid "Media Statistics"
    626693msgstr ""
    627694
    628695#. translators: %d: Number of files for a specific mime type.
    629 #: includes/Admin/views/tabs/tab-overview.php:230
     696#: includes/Admin/views/tabs/tab-overview.php:209
    630697#, php-format
    631698msgid "%d files"
    632699msgstr ""
    633700
    634 #: includes/Admin/views/tabs/tab-overview.php:238
     701#: includes/Admin/views/tabs/tab-overview.php:217
    635702msgid "No media files found yet."
    636703msgstr ""
    637704
    638 #: includes/Admin/views/tabs/tab-overview.php:250
     705#: includes/Admin/views/tabs/tab-overview.php:227
    639706msgid "Most Used Media"
    640707msgstr ""
    641708
    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
     710msgid "Loading usage statistics..."
    658711msgstr ""
    659712
     
    722775msgstr ""
    723776
    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
    741778msgid "Go Pro"
    742779msgstr ""
    743780
    744781#. translators: %s template file path
    745 #: includes/functions.php:222
     782#: includes/functions.php:255
    746783#, php-format
    747784msgid "%s does not exist."
    748785msgstr ""
    749786
    750 #: includes/functions.php:406
     787#: includes/functions.php:453
    751788msgid "This feature is available in Media Tracker Pro."
    752789msgstr ""
    753790
    754 #: includes/functions.php:425
     791#: includes/functions.php:472
    755792msgid "Upgrade to Media Tracker Pro"
    756793msgstr ""
    757794
    758 #: includes/Installer.php:181
     795#: includes/Installer.php:144
    759796msgid "Media Tracker Plugin Feedback"
    760797msgstr ""
    761798
    762 #: includes/Installer.php:200
     799#: includes/Installer.php:162
    763800msgid "If you have a moment, we'd love to know why you're deactivating the Media Tracker plugin!"
    764801msgstr ""
    765802
    766 #: includes/Installer.php:206
     803#: includes/Installer.php:166
    767804msgid "Enter your feedback here..."
    768805msgstr ""
    769806
    770 #: includes/Installer.php:210
     807#: includes/Installer.php:170
    771808msgid "Skip & Deactivate"
    772809msgstr ""
    773810
    774 #: includes/Installer.php:211
     811#: includes/Installer.php:171
    775812msgid "Submit & Deactivate"
    776813msgstr ""
  • media-tracker/trunk/media-tracker.php

    r3455634 r3457950  
    55 * Author: TheBitCraft
    66 * Author URI: https://thebitcraft.com/
    7  * Version: 1.3.1
     7 * Version: 1.3.2
    88 * Requires PHP: 7.4
    99 * Requires at least: 5.9
     
    2828     * @var string
    2929     */
    30     const version = '1.3.1';
     30    const version = '1.3.2';
    3131
    3232    /**
     
    9090     */
    9191    public function init_plugin() {
    92         // Check for updates
    93         $this->check_update();
    94 
    9592        new Media_Tracker\Media_Tracker_i18n();
    9693        new Media_Tracker\Assets();
     
    9996        if ( is_admin() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
    10097            new Media_Tracker\Admin();
    101         }
    102     }
    103 
    104     /**
    105      * Check if the plugin has been updated and run the installer if necessary
    106      *
    107      * @return void
    108      */
    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();
    11598        }
    11699    }
  • media-tracker/trunk/readme.txt

    r3455634 r3457950  
    66Tested up to: 6.9
    77Requires PHP: 7.4
    8 Stable tag: 1.3.1
     8Stable tag: 1.3.2
    99License: GPLv2 or later
    1010License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    6666
    6767== 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
    6875= 1.3.1 [07/02/2026] =
    6976* Fixed: Tab navigation loading issue.
Note: See TracChangeset for help on using the changeset viewer.