Plugin Directory

Changeset 3454648


Ignore:
Timestamp:
02/05/2026 01:11:59 PM (13 days ago)
Author:
thebitcraft
Message:

Design upgrade and bug fixed

Location:
media-tracker
Files:
113 added
18 edited

Legend:

Unmodified
Added
Removed
  • media-tracker/trunk/assets/dist/css/mt-admin.css

    r3432010 r3454648  
    1 .mediatracker-usage-table{background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-box-shadow:0 0 10px rgba(0,0,0,.1);box-shadow:0 0 10px rgba(0,0,0,.1);margin-top:20px;padding:10px}.mediatracker-usage-table table{border-collapse:collapse;width:100%}.mediatracker-usage-table table td,.mediatracker-usage-table table th{border-bottom:1px solid #ddd;padding:10px;text-align:left;text-transform:capitalize}.mediatracker-usage-table table th{background-color:#f4f4f4}.mediatracker-usage-table table tr:nth-child(2n){background-color:#f9f9f9}.mediatracker-usage-table table tr:hover{background-color:#f1f1f1}.mediatracker-usage-table table a{color:#0073aa;text-decoration:none}.mediatracker-usage-table table a:hover{text-decoration:underline}.unused-media-list .wp-list-table td strong{display:block;font-size:14px;margin-bottom:.2em}.unused-media-list .wp-list-table .media-icon{float:left;margin:0 9px 0 0;min-height:60px}.unused-media-list .wp-list-table .media-icon img{height:60px;width:60px}.unused-media-list .wp-list-table.fixed{table-layout:inherit}#mt-feedback-modal{-webkit-box-pack:center;-ms-flex-pack:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:rgba(0,0,0,.3);display:none;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:10000}#mt-feedback-modal .mt-feedback-modal-content{background:#fff;border-radius:8px;-webkit-box-shadow:0 4px 8px rgba(0,0,0,.2);box-shadow:0 4px 8px rgba(0,0,0,.2);margin:10% auto;max-width:600px;padding:30px;position:relative;text-align:center;width:80%}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header{text-align:left}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header h3{color:#333;font-size:1.5em;line-height:normal;margin-top:0}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header .close{cursor:pointer;font-size:1.5em;position:absolute;right:10px;top:10px}#mt-feedback-modal .mt-feedback-modal-content .mt-feedback-modal-body textarea{border:1px solid #ddd;border-radius:4px;font-size:1em;height:120px;margin:20px 0;padding:10px;resize:vertical;width:100%}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer{-webkit-box-align:start;-ms-flex-align:start;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:flex-start;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button{background-color:#007cba;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:1em;margin:5px;padding:10px 20px;-webkit-transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,transform .3s;transition:background-color .3s,transform .3s,-webkit-transform .3s}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:hover{background-color:#005a87;-webkit-transform:scale(1.05);transform:scale(1.05)}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:focus{outline:none}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button#mt-skip-feedback{background:transparent;border:1px solid #2271b1;color:#2271b1}.broken-link-checker .post_title{padding-left:15px!important}.broken-link-checker #post_type{padding:0}.wrap.broken-link-checker .wp-heading-inline,.wrap.unused-media-list .wp-heading-inline{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#28a745;border-radius:5px;color:#fff;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;gap:20px;margin-bottom:10px;margin-top:15px;padding:20px;width:calc(100% - 40px)}.wrap.broken-link-checker .wp-heading-inline svg,.wrap.unused-media-list .wp-heading-inline svg{height:40px;width:40px}.wrap.broken-link-checker .wp-filter,.wrap.unused-media-list .wp-filter{margin:10px 0}.wrap.broken-link-checker .notice h2,.wrap.unused-media-list .notice h2{margin-bottom:0}.wrap.broken-link-checker table.widefat,.wrap.unused-media-list table.widefat{margin-top:20px;table-layout:inherit;width:100%}.wrap.broken-link-checker table.widefat td,.wrap.broken-link-checker table.widefat th,.wrap.unused-media-list table.widefat td,.wrap.unused-media-list table.widefat th{padding:10px;text-align:left}.wrap.broken-link-checker table.widefat td.status,.wrap.broken-link-checker table.widefat th.status,.wrap.unused-media-list table.widefat td.status,.wrap.unused-media-list table.widefat th.status{color:red;font-weight:700}.media-toolbar-wrap.wp-filter{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;gap:20px;justify-content:space-between;padding:0 15px}.media-toolbar-wrap.wp-filter .search-form input[type=search]{width:215px}.unused-image-found h2{font-size:24px;font-weight:300}.unused-image-found h2 span{color:#cf0000;font-size:28px;font-weight:700}.replace-broken-link input{width:100%}#clear-broken-links-transient{font-size:14px;font-weight:500;padding:8px 30px;position:absolute}#success-message{color:green;display:none;font-size:16px;left:230px;margin-top:15px;position:absolute}.wp-list-table #usage_count{width:130px}
     1#mt-feedback-modal{-webkit-box-pack:center;-ms-flex-pack:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:rgba(0,0,0,.3);display:none;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:10000}#mt-feedback-modal .mt-feedback-modal-content{background:#fff;border-radius:8px;-webkit-box-shadow:0 4px 8px rgba(0,0,0,.2);box-shadow:0 4px 8px rgba(0,0,0,.2);margin:10% auto;max-width:600px;padding:30px;position:relative;text-align:center;width:80%}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header{text-align:left}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header h3{color:#333;font-size:1.5em;line-height:normal;margin-top:0}#mt-feedback-modal .mt-feedback-modal-content header.mt-feedback-modal-header .close{cursor:pointer;font-size:1.5em;position:absolute;right:10px;top:10px}#mt-feedback-modal .mt-feedback-modal-content .mt-feedback-modal-body textarea{border:1px solid #ddd;border-radius:4px;font-size:1em;height:120px;margin:20px 0;padding:10px;resize:vertical;width:100%}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer{-webkit-box-align:start;-ms-flex-align:start;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:flex-start;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button{background-color:#007cba;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:1em;margin:5px;padding:10px 20px;-webkit-transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,-webkit-transform .3s;transition:background-color .3s,transform .3s;transition:background-color .3s,transform .3s,-webkit-transform .3s}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:hover{background-color:#005a87;-webkit-transform:scale(1.05);transform:scale(1.05)}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button:focus{outline:none}#mt-feedback-modal .mt-feedback-modal-content footer.mt-feedback-modal-footer button#mt-skip-feedback{background:transparent;border:1px solid #2271b1;color:#2271b1}.broken-link-checker .post_title{padding-left:15px!important}.broken-link-checker #post_type{padding:0}.wrap.broken-link-checker .wp-heading-inline,.wrap.unused-media-list .wp-heading-inline{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#28a745;border-radius:5px;color:#fff;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;gap:20px;margin-bottom:10px;margin-top:15px;padding:20px;width:calc(100% - 40px)}.wrap.broken-link-checker .wp-heading-inline svg,.wrap.unused-media-list .wp-heading-inline svg{height:40px;width:40px}.media-tracker-layout{background-color:#f8fafc;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-sizing:inherit;box-sizing:inherit;color:#1e293b;margin:24px 0 0;min-height:100vh;width:98.8%}.media-tracker-layout,.media-tracker-layout h2{display:-webkit-box;display:-ms-flexbox;display:flex;padding:0}.media-tracker-layout h2{-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:6px;margin:0}.media-tracker-layout aside{-webkit-box-orient:vertical;-webkit-box-direction:normal;background:#0f172a;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:12px;width:260px}.media-tracker-layout aside .version{padding:15px;text-align:center}.media-tracker-layout .logo{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:19.2px;font-size:1.2rem;font-weight:700;gap:8px;margin:10px 0 20px}.media-tracker-layout nav{-webkit-box-flex:1;-ms-flex:1;flex:1}.media-tracker-layout nav ul{list-style:none;margin:0}.media-tracker-layout nav li{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:start;-ms-flex-pack:start;align-items:center;border-radius:4px;color:#fff;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:14px;gap:10px;justify-content:flex-start;letter-spacing:.1px;margin:0 0 3px;padding:13px 10px;-webkit-transition:.25s;transition:.25s}.media-tracker-layout nav li.active,.media-tracker-layout nav li:hover{background:#6366f1;color:#fff}.media-tracker-layout nav li i{text-align:left;width:20px}.media-tracker-layout ul li a{border:none;color:#fff;gap:10px;outline:none;padding:15px;text-decoration:none;width:100%}.media-tracker-layout ul li a,.media-tracker-layout ul li a small{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout ul li a small{margin-left:auto}.media-tracker-layout ul li a:focus{-webkit-box-shadow:none;box-shadow:none;outline:none}.media-tracker-layout ul li.license,.media-tracker-layout ul li.multisite,.media-tracker-layout ul li.settings{padding:15px!important}.media-tracker-layout ul li:last-child{padding:0}.media-tracker-layout ul li:hover a{color:#fff}.media-tracker-layout main{-webkit-box-flex:1;background:#fff;-ms-flex:1;flex:1;overflow-y:auto;padding:2rem}.media-tracker-layout header{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;margin-bottom:2rem}.media-tracker-layout .status-badge{background:#dcfce7;border-radius:20px;color:#166534;font-size:12px;font-weight:600;padding:4px 12px}.media-tracker-layout h1{font-size:22px;font-weight:600;letter-spacing:-.01em;margin:0}.media-tracker-layout .stats-grid{display:grid;gap:24px;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin-bottom:1.5rem}.media-tracker-layout .card{background:#fff;border:1px solid #e2e8f0;border-radius:10px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.05);box-shadow:0 1px 3px rgba(0,0,0,.05);margin:0;max-width:100%;padding:1.5rem}.media-tracker-layout .card table .btn{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;font-size:12px;width:80px}.media-tracker-layout .card h3{color:#313335;font-size:16px;margin-bottom:12px;margin-top:0}.media-tracker-layout .card h3 i{color:#6366f1}.media-tracker-layout .card .value{display:block;font-size:18px;font-weight:700;line-height:normal;margin-bottom:3px}.media-tracker-layout .progress-bar{background:#e5e7eb;border-radius:4px;height:8px;margin-top:10px;overflow:hidden}.media-tracker-layout .progress-fill{background:#6366f1;border-radius:4px;height:100%}.media-tracker-layout .section-title{font-size:16px}.media-tracker-layout .grid-two{display:grid;gap:24px;gap:1.5rem;grid-template-columns:2.05fr 1fr}.media-tracker-layout .grid-three{display:grid;gap:24px;gap:1.5rem;grid-template-columns:1fr 1fr 1fr}.media-tracker-layout .setting-item{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border-bottom:1px solid #e2e8f0;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;padding:12px 0}.media-tracker-layout .setting-item p{margin:0}.media-tracker-layout .setting-item:last-child{border:none}.media-tracker-layout .switch{display:inline-block;height:22px;position:relative;width:44px}.media-tracker-layout .switch input{height:0;opacity:0;width:0}.media-tracker-layout .slider{background-color:#cbd5f5;border-radius:34px;bottom:0;cursor:pointer;left:0;position:absolute;right:0;top:0;-webkit-transition:.25s;transition:.25s}.media-tracker-layout .slider:before{background-color:#fff;border-radius:50%;bottom:3px;content:"";height:16px;left:3px;position:absolute;-webkit-transition:.25s;transition:.25s;width:16px}.media-tracker-layout input:checked+.slider{background-color:#6366f1}.media-tracker-layout input:checked+.slider:before{-webkit-transform:translateX(22px);transform:translateX(22px)}.media-tracker-layout .btn{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;border:none;border-radius:6px;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;font-size:14px;font-weight:600;gap:5px;justify-content:center;padding:12px 20px;text-decoration:none;-webkit-transition:.2s;transition:.2s}.media-tracker-layout .btn i{font-size:14px;height:auto}.media-tracker-layout .btn-primary{background:#6366f1;color:#fff}.media-tracker-layout .btn-primary:hover{background:#4f46e5}.media-tracker-layout .btn-outline{background:transparent;border:1px solid #e2e8f0;color:#1e293b}.media-tracker-layout .btn-outline:hover{background:#f1f5f9}.media-tracker-layout .btn-danger{background:#ef4444;color:#fff}.media-tracker-layout table{border-collapse:collapse;margin-top:1rem;width:100%}.media-tracker-layout th{color:#64748b;font-size:14px;padding:12px;text-align:left}.media-tracker-layout th:last-child{text-align:center}.media-tracker-layout td{border-bottom:1px solid #c3c4c7;font-size:14px;padding:12px}.media-tracker-layout tr:last-child td{border-bottom:none}.media-tracker-layout .tag{border-radius:4px;font-size:11px;font-weight:700;padding:2px 8px;text-transform:uppercase}.media-tracker-layout .tag-unused{background:#fee2e2;color:#991b1b}.media-tracker-layout .tag-duplicate{background:#fef3c7;color:#92400e}.media-tracker-layout .tab-content{display:none}.media-tracker-layout .tab-content.active{display:block}.media-tracker-layout .page-subtitle{color:#64748b;font-size:13px;margin-bottom:0;margin-top:10px}.media-tracker-layout .stacked{-webkit-box-orient:vertical;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:10px;margin-top:10px}.media-tracker-layout .inline-actions{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:end;-ms-flex-pack:end;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:10px;justify-content:flex-end}.media-tracker-layout .modal-backdrop{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;background:rgba(15,23,42,.55);display:none;inset:0;justify-content:center;position:fixed;z-index:50}.media-tracker-layout .modal-backdrop.active{display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .modal{background:#fff;border:1px solid #e2e8f0;border-radius:12px;-webkit-box-shadow:0 20px 40px rgba(15,23,42,.3);box-shadow:0 20px 40px rgba(15,23,42,.3);max-width:94%;padding:1.5rem;width:480px}.media-tracker-layout .modal-header{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:1rem}.media-tracker-layout .modal-header,.media-tracker-layout .modal-title{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .modal-title{font-size:18px;font-weight:600;gap:8px}.media-tracker-layout .modal-close{background:transparent;border:none;color:#64748b;cursor:pointer;font-size:18px}.media-tracker-layout .modal-body{-webkit-box-orient:vertical;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:12px;margin-bottom:1.5rem}.media-tracker-layout .modal-body label{display:block;font-size:13px;margin-bottom:4px}.media-tracker-layout .modal-body input,.media-tracker-layout .modal-body select{border:1px solid #e2e8f0;border-radius:6px;font-size:13px;padding:8px 10px;width:100%}.media-tracker-layout .modal-body input:focus,.media-tracker-layout .modal-body select:focus{border-color:#6366f1;-webkit-box-shadow:0 0 0 1px rgba(99,102,241,.25);box-shadow:0 0 0 1px rgba(99,102,241,.25);outline:none}.media-tracker-layout .modal-hint{color:#64748b;font-size:11px}.media-tracker-layout .modal-footer{-webkit-box-pack:end;-ms-flex-pack:end;display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;justify-content:flex-end}.media-tracker-layout .mediatracker-usage-table{margin-top:15px}.media-tracker-layout .mediatracker-usage-table table.wp-list-table td,.media-tracker-layout .mediatracker-usage-table table.wp-list-table th{vertical-align:middle}.media-tracker-layout .mediatracker-usage-table table.wp-list-table .button{margin-right:4px}.media-tracker-layout .unused-media-list .wp-list-table td strong{display:block;font-size:14px;margin-bottom:.2em}.media-tracker-layout .unused-media-list .wp-list-table .media-icon{float:left;margin:0 9px 0 0;min-height:60px}.media-tracker-layout .unused-media-list .wp-list-table .media-icon img{height:60px;width:60px}.media-tracker-layout .unused-media-list .wp-list-table.fixed{table-layout:inherit}.media-tracker-layout .unused-media-list .search-box{margin:0}.media-tracker-layout .unused-media-list .wp-filter{margin:0 0 20px}.media-tracker-layout .unused-media-list .notice h2{margin-bottom:0}.media-tracker-layout .unused-media-list table.widefat{margin-top:20px;table-layout:inherit;width:100%}.media-tracker-layout .unused-media-list table.widefat td,.media-tracker-layout .unused-media-list table.widefat th{padding:10px;text-align:left}.media-tracker-layout .unused-media-list table.widefat td.status,.media-tracker-layout .unused-media-list table.widefat th.status{color:red;font-weight:700}.media-tracker-layout .media-toolbar-wrap.wp-filter{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border:none;-webkit-box-shadow:none;box-shadow:none;display:-webkit-box;display:-ms-flexbox;display:flex;gap:20px;justify-content:space-between}.media-tracker-layout .media-toolbar-wrap.wp-filter .search-form input[type=search]{width:215px}.media-tracker-layout .unused-image-found h2{font-size:20px;font-weight:400}.media-tracker-layout .unused-image-found h2 span{color:#6366f1;font-size:20px;font-weight:700}.media-tracker-layout .replace-broken-link input{width:100%}.media-tracker-layout #clear-broken-links-transient{font-size:14px;font-weight:500;padding:8px 30px;position:absolute}.media-tracker-layout #success-message{color:green;display:none;font-size:16px;left:230px;margin-top:15px;position:absolute}.media-tracker-layout .wp-list-table #usage_count{width:130px}.media-tracker-layout #mt-duplicate-form table tbody td:last-child,.media-tracker-layout #mt-duplicate-form table tr th:last-child{text-align:center}.media-tracker-layout #mt-duplicate-form table .check-column{background:transparent;border-bottom:1px solid #c3c4c7;padding:16px 3px;width:3.2em}.media-tracker-layout #mt-duplicate-form .duplicate-media-footer{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;gap:30px;justify-content:space-between;margin-top:15px}.media-tracker-layout #mt-duplicate-form .tablenav-pages .paging-input{margin:0 15px}.media-tracker-layout .duplicate-media-count h2{color:#6366f1;font-size:20px;font-weight:700}.media-tracker-layout .duplicate-media-count h2 span{color:#1d2327;font-weight:400}.media-tracker-layout .media-header{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;margin-bottom:30px}.media-tracker-layout #tab-unused-media .tablenav.bottom{margin-top:15px}.media-tracker-layout .mt-overview-table{background:#fff;border-collapse:separate;border-radius:8px;border-spacing:0;-webkit-box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);margin-top:20px;overflow:hidden;width:100%}.media-tracker-layout .mt-overview-table thead th{background-color:#f8fafc;border-bottom:1px solid #e2e8f0;color:#475569;font-size:11px;font-weight:600;letter-spacing:.05em;padding:16px;text-align:left;text-transform:uppercase}.media-tracker-layout .mt-overview-table thead tr:first-child th:first-child{border-top-left-radius:8px}.media-tracker-layout .mt-overview-table thead tr:first-child th:last-child{border-top-right-radius:8px}.media-tracker-layout .mt-overview-table tbody tr{-webkit-transition:background-color .2s;transition:background-color .2s}.media-tracker-layout .mt-overview-table tbody tr:nth-child(2n){background-color:#f8fafc}.media-tracker-layout .mt-overview-table tbody tr:hover{background-color:#f1f5f9}.media-tracker-layout .mt-overview-table tbody tr:last-child td{border-bottom:none}.media-tracker-layout .mt-overview-table tbody td{border-bottom:1px solid #f1f5f9;color:#334155;font-size:14px;padding:16px;vertical-align:middle}.media-tracker-layout .mt-overview-table tbody td:last-child{text-align:center}.media-tracker-layout .mt-overview-table tbody td strong{color:#0f172a;font-weight:500}.media-tracker-layout .mt-overview-table tbody td small{color:#94a3b8;display:block;margin-top:4px}.media-tracker-layout .mt-overview-table tbody tr:last-child td:first-child{border-bottom-left-radius:8px}.media-tracker-layout .mt-overview-table tbody tr:last-child td:last-child{border-bottom-right-radius:8px}.media-tracker-layout .mt-flex-col-end{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:8px}.media-tracker-layout .mt-flex-center{gap:10px}.media-tracker-layout .mt-flex-center,.media-tracker-layout .mt-stat-title{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.media-tracker-layout .mt-stat-title{gap:8px}.media-tracker-layout .mt-icon-indigo{color:#6366f1}.media-tracker-layout .mt-icon-amber{color:#f59e0b}.media-tracker-layout .mt-stat-subtitle{color:#64748b;font-size:12px}.media-tracker-layout .mt-mt-10{margin-top:10px}.media-tracker-layout .mt-helper-text{color:#64748b;font-size:12px}.media-tracker-layout .mt-btn-sm{font-size:12px!important;padding:6px 12px!important}.media-tracker-layout .mt-btn-xs{padding:5px 10px!important}.media-tracker-layout .mt-mime-item{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f8fafc;border-radius:8px;display:-webkit-box;display:-ms-flexbox;display:flex;gap:10px;padding:10px}.media-tracker-layout .mt-mime-icon{color:#6366f1;text-align:center;width:20px}.media-tracker-layout .mt-flex-1{-webkit-box-flex:1;-ms-flex:1;flex:1}.media-tracker-layout .mt-font-medium{font-size:14px;font-weight:500}.media-tracker-layout .mt-empty-state{color:#64748b;font-size:12px;padding:20px;text-align:center}.media-tracker-layout .mt-mt-6{margin-top:1.5rem}.media-tracker-layout .mt-empty-state-large{color:#64748b;padding:40px;text-align:center}.media-tracker-layout .mt-success-icon-large{color:#10b981;font-size:48px;height:48px;margin-bottom:15px;width:48px}.media-tracker-layout .mt-tag-success{background:#10b981;color:#fff}.media-tracker-layout .mt-btn-xs-clean{padding:5px 10px!important;text-decoration:none}.media-tracker-layout .mt-chart-icon-large{color:#cbd5e1;font-size:48px;height:48px;margin-bottom:15px;width:48px}.media-tracker-layout .mt-mb-3{margin-bottom:12px}.media-tracker-layout .mt-progress-text{color:#64748b;display:none;font-size:11px;margin-left:8px;min-width:40px;text-align:right}.media-tracker-layout .mt-status-text{display:none;margin-left:4px}.media-tracker-layout .mt-progress-track{-webkit-box-flex:1;background:-webkit-gradient(linear,left top,right top,from(#eef2ff),to(#e2e8f0));background:linear-gradient(90deg,#eef2ff,#e2e8f0);border-radius:999px;-webkit-box-shadow:0 0 0 1px rgba(148,163,184,.4);box-shadow:0 0 0 1px rgba(148,163,184,.4);display:none;-ms-flex:1;flex:1;height:15px;max-width:100%;overflow:hidden}.media-tracker-layout .mt-progress-bar-fill{background:#6366f1;height:100%;width:0}.media-tracker-layout .mt-v-middle{vertical-align:middle}.media-tracker-layout .mt-text-center{text-align:center}.media-tracker-layout .mt-mr-1{margin-right:4px}.media-tracker-layout .mt-thumb-img{height:auto;width:60px}.media-tracker-layout .mt-link-clean{font-weight:500;text-decoration:none}@media(max-width:960px){.media-tracker-layout{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.media-tracker-layout aside{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:justify;-ms-flex-pack:justify;align-items:center;-ms-flex-direction:row;flex-direction:row;justify-content:space-between;padding:1rem 1.5rem;width:100%}.media-tracker-layout nav ul{display:-webkit-box;display:-ms-flexbox;display:flex;overflow-x:auto}.media-tracker-layout nav li{white-space:nowrap}}.media-video-featured-card{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;background:#fff;border:1px solid #e2e8f0;border-radius:12px;-webkit-box-shadow:0 4px 6px -1px rgba(0,0,0,.05);box-shadow:0 4px 6px -1px rgba(0,0,0,.05);cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;overflow:hidden;-webkit-transition:all .3s ease;transition:all .3s ease;width:32%}.media-video-featured-card:hover{border-color:#cbd5e1;-webkit-box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);-webkit-transform:translateY(-2px);transform:translateY(-2px)}.media-video-featured-card:hover .video-thumbnail{opacity:.6}.media-video-featured-card:hover .play-icon{background:#fff;-webkit-box-shadow:0 0 0 8px hsla(0,0%,100%,.3);box-shadow:0 0 0 8px hsla(0,0%,100%,.3);-webkit-transform:scale(1.1);transform:scale(1.1)}.media-video-featured-card .video-thumbnail-wrapper{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;background:#000;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center;overflow:hidden;position:relative}.media-video-featured-card .video-thumbnail{height:100%;-o-object-fit:cover;object-fit:cover;opacity:.8;-webkit-transition:opacity .3s ease;transition:opacity .3s ease;width:100%}.media-video-featured-card .play-button-overlay{height:100%;left:0;position:absolute;top:0;width:100%;z-index:2}.media-video-featured-card .play-button-overlay,.media-video-featured-card .play-icon{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center}.media-video-featured-card .play-icon{background:hsla(0,0%,100%,.9);border-radius:50%;-webkit-box-shadow:0 0 0 0 hsla(0,0%,100%,.7);box-shadow:0 0 0 0 hsla(0,0%,100%,.7);height:60px;-webkit-transition:all .3s cubic-bezier(.175,.885,.32,1.275);transition:all .3s cubic-bezier(.175,.885,.32,1.275);width:60px}.media-video-featured-card .play-icon .dashicons{color:#6366f1;font-size:32px;height:32px;margin-left:4px;width:32px}.media-video-featured-card .video-card-content{-webkit-box-flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex:1;flex:1;-ms-flex-direction:column;flex-direction:column;justify-content:center;padding:2rem}.media-video-featured-card .video-card-content h4{color:#1e293b;font-size:20px;font-size:1.25rem;font-weight:600;margin:0 0 .75rem}.mt-video-modal-backdrop{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;backdrop-filter:blur(4px);background:rgba(15,23,42,.75);display:none;inset:0;justify-content:center;opacity:0;position:fixed;-webkit-transition:opacity .3s ease;transition:opacity .3s ease;z-index:100000}.mt-video-modal-backdrop.active{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.mt-video-modal-backdrop.active .mt-video-modal{-webkit-transform:scale(1);transform:scale(1)}.mt-video-modal{background:#fff;border-radius:16px;-webkit-box-shadow:0 25px 50px -12px rgba(0,0,0,.25);box-shadow:0 25px 50px -12px rgba(0,0,0,.25);max-width:800px;overflow:hidden;-webkit-transform:scale(.95);transform:scale(.95);-webkit-transition:-webkit-transform .3s cubic-bezier(.34,1.56,.64,1);transition:-webkit-transform .3s cubic-bezier(.34,1.56,.64,1);transition:transform .3s cubic-bezier(.34,1.56,.64,1);transition:transform .3s cubic-bezier(.34,1.56,.64,1),-webkit-transform .3s cubic-bezier(.34,1.56,.64,1);width:90%}.mt-video-modal-header{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-between;padding:1rem 1.5rem}.mt-video-modal-header h3{color:#334155;font-size:17.6px;font-size:1.1rem;margin:0}.mt-video-modal-close{-webkit-box-align:center;-ms-flex-align:center;-webkit-box-pack:center;-ms-flex-pack:center;align-items:center;background:transparent;border:none;border-radius:4px;color:#64748b;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center;padding:4px;-webkit-transition:background .2s;transition:background .2s}.mt-video-modal-close:hover{background:#e2e8f0;color:#ef4444}.mt-video-modal-body{background:#000;padding:0}.mt-responsive-video-wrapper{height:0;overflow:hidden;padding-bottom:56.25%;position:relative}.mt-responsive-video-wrapper iframe{border:0;height:100%;left:0;position:absolute;top:0;width:100%}@media(max-width:768px){.media-video-featured-card{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.media-video-featured-card .video-thumbnail-wrapper{height:200px;width:100%}.media-video-featured-card .video-card-content{padding:1.5rem}}
    22/*# sourceMappingURL=mt-admin.css.map */
  • media-tracker/trunk/assets/dist/css/mt-admin.css.map

    r3159474 r3454648  
    1 {"version":3,"sources":["mt-admin.scss"],"names":[],"mappings":"AAKA,0BAKI,qBAAA,CAHA,qBAAA,CACA,iBAAA,CAGA,0CAAA,CAAA,kCAAA,CALA,eAAA,CAGA,YAEA,CAEA,gCAEI,wBAAA,CADA,UACA,CAEA,sEAGI,4BAAA,CAFA,YAAA,CACA,eAAA,CAEA,yBAAA,CAGJ,mCACI,wBAAA,CAGJ,iDACI,wBAAA,CAGJ,yCACI,wBAAA,CAGJ,kCACI,aAAA,CACA,oBAAA,CAEA,wCACI,yBAAA,CASR,4CAEI,aAAA,CAEA,cAAA,CADA,kBACA,CAGJ,8CACI,UAAA,CAEA,gBAAA,CADA,eACA,CAEA,kDAEI,WAAA,CADA,UACA,CAIR,wCACI,oBAAA,CAMZ,mBASI,uBAAA,CAAA,oBAAA,CACA,wBAAA,CAAA,qBAAA,CAAA,kBAAA,CAHA,+BAAA,CACA,YAAA,CAFA,WAAA,CAGA,sBAAA,CANA,MAAA,CAFA,cAAA,CAGA,KAAA,CACA,UAAA,CAHA,aAQA,CAGA,8CACI,eAAA,CAEA,iBAAA,CAGA,2CAAA,CAAA,mCAAA,CAGA,eAAA,CAJA,eAAA,CAHA,YAAA,CAKA,iBAAA,CACA,iBAAA,CAJA,SAKA,CAGA,8EACI,eAAA,CACA,iFAGI,UAAA,CADA,eAAA,CAEA,kBAAA,CAHA,YAGA,CAIJ,qFAKI,cAAA,CADA,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,QAGA,CAMJ,+EAKI,qBAAA,CACA,iBAAA,CACA,aAAA,CALA,YAAA,CACA,aAAA,CACA,YAAA,CAIA,eAAA,CAPA,UAOA,CAKR,8EAEI,uBAAA,CAAA,oBAAA,CACA,wBAAA,CAAA,qBAAA,CADA,sBAAA,CADA,mBAAA,CAAA,mBAAA,CAAA,YAAA,CAEA,6BAAA,CAEA,qFACI,wBAAA,CAEA,WAAA,CACA,iBAAA,CAFA,UAAA,CAKA,cAAA,CADA,aAAA,CAEA,UAAA,CAHA,iBAAA,CAIA,6DAAA,CAAA,qDAAA,CAAA,6CAAA,CAAA,mEAAA,CAEA,2FACI,wBAAA,CACA,6BAAA,CAAA,qBAAA,CAGJ,2FACI,YAAA,CAIJ,sGACI,sBAAA,CAEA,wBAAA,CADA,aACA,CAQhB,iCACI,2BAAA,CAGJ,gCACI,SAAA,CAMJ,wFAKI,wBAAA,CAAA,qBAAA,CAAA,kBAAA,CAHA,kBAAA,CAMA,iBAAA,CALA,UAAA,CACA,0BAAA,CAAA,0BAAA,CAAA,mBAAA,CAGA,QAAA,CAGA,kBAAA,CADA,eAAA,CAHA,YAAA,CALA,uBASA,CAEA,gGAEI,WAAA,CADA,UACA,CAIR,wEACI,aAAA,CAIA,wEACI,eAAA,CAKR,8EAGI,eAAA,CAFA,oBAAA,CACA,UACA,CAGJ,wKAEI,YAAA,CACA,eAAA,CAEA,oMAEI,SAAA,CADA,eACA,CAKZ,8BAEI,wBAAA,CAAA,qBAAA,CACA,wBAAA,CAAA,qBAAA,CAAA,kBAAA,CAFA,mBAAA,CAAA,mBAAA,CAAA,YAAA,CAGA,QAAA,CAFA,6BAAA,CAGA,cAAA,CAGJ,uBACI,cAAA,CACA,eAAA,CAEA,4BAGI,aAAA,CAFA,cAAA,CACA,eACA,CAKJ,2BACI,UAAA,CAIR,8BAGI,cAAA,CACA,eAAA,CAFA,gBAAA,CADA,iBAGA,CAGJ,iBAEI,WAAA,CADA,YAAA,CAKA,cAAA,CADA,UAAA,CAFA,eAAA,CACA,iBAEA","file":"mt-admin.css"}
     1{"version":3,"sources":["mt-admin.scss"],"names":[],"mappings":"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","file":"mt-admin.css"}
  • media-tracker/trunk/assets/src/scss/mt-admin.scss

    r3432010 r3454648  
    44*/
    55
    6 .mediatracker-usage-table {
    7     margin-top: 20px;
    8     border: 1px solid #ddd;
    9     border-radius: 4px;
    10     padding: 10px;
    11     background-color: #fff;
    12     box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    13 
    14     table {
    15         width: 100%;
    16         border-collapse: collapse;
    17 
    18         th, td {
    19             padding: 10px;
    20             text-align: left;
    21             border-bottom: 1px solid #ddd;
    22             text-transform: capitalize;
    23         }
    24 
    25         th {
    26             background-color: #f4f4f4;
    27         }
    28 
    29         tr:nth-child(even) {
    30             background-color: #f9f9f9;
    31         }
    32 
    33         tr:hover {
    34             background-color: #f1f1f1;
    35         }
    36 
    37         a {
    38             color: #0073aa;
    39             text-decoration: none;
    40 
    41             &:hover {
    42                 text-decoration: underline;
    43             }
    44         }
    45     }
    46 }
    47 
    48 // Unused Media List CSS
    49 .unused-media-list {
    50     .wp-list-table {
    51         td strong,
    52         td strong {
    53             display: block;
    54             margin-bottom: .2em;
    55             font-size: 14px;
    56         }
    57 
    58         .media-icon {
    59             float: left;
    60             min-height: 60px;
    61             margin: 0 9px 0 0;
    62 
    63             img {
    64                 width: 60px;
    65                 height: 60px;
    66             }
    67         }
    68 
    69         &.fixed {
    70             table-layout: inherit;
    71         }
    72     }
     6:root {
     7    --primary: #6366f1;
     8    --primary-dark: #4f46e5;
     9    --bg: #f8fafc;
     10    --card: #ffffff;
     11    --text-main: #1e293b;
     12    --text-muted: #64748b;
     13    --border: #e2e8f0;
     14    --success: #22c55e;
     15    --danger: #ef4444;
     16    --warning: #f59e0b;
    7317}
    7418
     
    179123}
    180124
    181 .wrap.unused-media-list,
    182 .wrap.broken-link-checker {
    183     .wp-heading-inline {
    184         width: calc(100% - 40px);
    185         background: #28a745;
     125.wrap {
     126    &.unused-media-list,
     127    &.broken-link-checker {
     128        .wp-heading-inline {
     129            width: calc(100% - 40px);
     130            background: #28a745;
     131            color: #fff;
     132            display: inline-flex;
     133            align-items: center;
     134            padding: 20px 20px;
     135            gap: 20px;
     136            border-radius: 5px;
     137            margin-top: 15px;
     138            margin-bottom: 10px;
     139
     140            svg {
     141                width: 40px;
     142                height: 40px;
     143            }
     144        }
     145    }
     146}
     147
     148.media-tracker-layout {
     149    margin: 0;
     150    padding: 0;
     151    box-sizing: border-box;
     152    box-sizing: inherit;
     153    background-color: var(--bg);
     154    color: var(--text-main);
     155    display: flex;
     156    min-height: 100vh;
     157    margin-top: 24px;
     158    width: 98.8%;
     159
     160    h2 {
     161        margin: 0;
     162        padding: 0;
     163        display: flex;
     164        align-items: center;
     165        gap: 6px;
     166    }
     167
     168    aside {
     169        width: 260px;
     170        background: #0f172a;
     171        color: white;
     172        padding: 0;
     173        display: flex;
     174        flex-direction: column;
     175        padding: 12px;
     176
     177        .version {
     178            text-align: center;
     179            padding: 15px;
     180        }
     181    }
     182
     183    .logo {
     184        font-size: 1.2rem;
     185        font-weight: bold;
     186        margin: 10px 0px 20px;
     187        display: flex;
     188        align-items: center;
     189        gap: 8px;
     190        color: #ffffff;
     191    }
     192
     193    nav {
     194        flex: 1;
     195
     196        ul {
     197            list-style: none;
     198            margin: 0;
     199        }
     200
     201        li {
     202            padding: 13px 10px;
     203            cursor: pointer;
     204            transition: 0.25s;
     205            display: flex;
     206            align-items: center;
     207            justify-content: flex-start;
     208            gap: 10px;
     209            font-size: 14px;
     210            color: #ffffff;
     211            letter-spacing: .1px;
     212            margin: 0 0 3px;
     213            border-radius: 4px;
     214
     215            &:hover,
     216            &.active {
     217                background: #6366f1;
     218                color: #ffffff;
     219            }
     220
     221            i {
     222                width: 20px;
     223                text-align: left;
     224            }
     225        }
     226    }
     227
     228    ul {
     229        li {
     230            a {
     231                color: #fff;
     232                text-decoration: none;
     233                width: 100%;
     234                padding: 15px;
     235                outline: none;
     236                border: none;
     237                display: flex;
     238                align-items: center;
     239                text-decoration: none;
     240                gap: 10px;
     241
     242                small {
     243                    margin-left: auto;
     244                    display: flex;
     245                    align-items: center;
     246                }
     247
     248                &:focus {
     249                    outline: none;
     250                    box-shadow: none;
     251                }
     252            }
     253
     254            &.license,
     255            &.settings,
     256            &.multisite {
     257                padding: 15px !important;
     258            }
     259
     260            &:last-child {
     261                padding: 0;
     262            }
     263
     264            &:hover {
     265                a {
     266                    color: #ffffff;
     267                }
     268            }
     269        }
     270    }
     271
     272    main {
     273        flex: 1;
     274        padding: 2rem;
     275        overflow-y: auto;
     276        background: #ffffff;
     277    }
     278
     279    header {
     280        display: flex;
     281        justify-content: space-between;
     282        align-items: center;
     283        margin-bottom: 2rem;
     284    }
     285
     286    .status-badge {
     287        background: #dcfce7;
     288        color: #166534;
     289        padding: 4px 12px;
     290        border-radius: 20px;
     291        font-size: 12px;
     292        font-weight: 600;
     293    }
     294
     295    h1 {
     296        font-size: 22px;
     297        font-weight: 600;
     298        letter-spacing: -0.01em;
     299        margin: 0;
     300    }
     301
     302    .stats-grid {
     303        display: grid;
     304        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
     305        gap: 1.5rem;
     306        margin-bottom: 1.5rem;
     307    }
     308
     309    .card {
     310        max-width: 100%;
     311        background: #ffffff;
     312        padding: 1.5rem;
     313        border-radius: 10px;
     314        border: 1px solid #e2e8f0;
     315        box-shadow: 0 1px 3px rgba(0, 0, 0, .05);
     316        margin: 0;
     317
     318        table {
     319            .btn {
     320                font-size: 12px;
     321                width: 80px;
     322                display: inline-flex;
     323            }
     324        }
     325
     326        h3 {
     327            font-size: 16px;
     328            color: #313335;
     329            margin-bottom: 12px;
     330            margin-top: 0;
     331
     332            i {
     333                color: #6366f1;
     334            }
     335        }
     336
     337        .value {
     338            font-size: 18px;
     339            line-height: normal;
     340            font-weight: bold;
     341            display: block;
     342            margin-bottom: 3px;
     343        }
     344    }
     345
     346    .progress-bar {
     347        height: 8px;
     348        background: #e5e7eb;
     349        border-radius: 4px;
     350        margin-top: 10px;
     351        overflow: hidden;
     352    }
     353
     354    .progress-fill {
     355        height: 100%;
     356        background: #6366f1;
     357        border-radius: 4px;
     358    }
     359
     360    .section-title {
     361        font-size: 16px;
     362    }
     363
     364    .grid-two {
     365        display: grid;
     366        grid-template-columns: 2.05fr 1fr;
     367        gap: 1.5rem;
     368    }
     369
     370    .grid-three {
     371        display: grid;
     372        grid-template-columns: 1fr 1fr 1fr;
     373        gap: 1.5rem;
     374    }
     375
     376    .setting-item {
     377        display: flex;
     378        justify-content: space-between;
     379        align-items: center;
     380        padding: 12px 0;
     381        border-bottom: 1px solid #e2e8f0;
     382
     383        p {
     384            margin: 0;
     385        }
     386
     387        &:last-child {
     388            border: none;
     389        }
     390    }
     391
     392    .switch {
     393        position: relative;
     394        display: inline-block;
     395        width: 44px;
     396        height: 22px;
     397
     398        input {
     399            opacity: 0;
     400            width: 0;
     401            height: 0;
     402        }
     403    }
     404
     405    .slider {
     406        position: absolute;
     407        cursor: pointer;
     408        top: 0;
     409        left: 0;
     410        right: 0;
     411        bottom: 0;
     412        background-color: #cbd5f5;
     413        transition: .25s;
     414        border-radius: 34px;
     415
     416        &:before {
     417            position: absolute;
     418            content: "";
     419            height: 16px;
     420            width: 16px;
     421            left: 3px;
     422            bottom: 3px;
     423            background-color: #fff;
     424            transition: .25s;
     425            border-radius: 50%;
     426        }
     427    }
     428
     429    input:checked + .slider {
     430        background-color: #6366f1;
     431
     432        &:before {
     433            transform: translateX(22px);
     434        }
     435    }
     436
     437    .btn {
     438        padding: 12px 20px;
     439        border-radius: 6px;
     440        border: none;
     441        font-weight: 600;
     442        cursor: pointer;
     443        transition: .2s;
     444        font-size: 14px;
     445        display: flex;
     446        align-items: center;
     447        justify-content: center;
     448        gap: 5px;
     449        text-decoration: none;
     450
     451        i {
     452            font-size: 14px;
     453            height: auto;
     454        }
     455    }
     456
     457    .btn-primary {
     458        background: #6366f1;
    186459        color: #fff;
    187         display: inline-flex;
    188         align-items: center;
    189         padding: 20px 20px;
    190         gap: 20px;
    191         border-radius: 5px;
     460
     461        &:hover {
     462            background: #4f46e5;
     463        }
     464    }
     465
     466    .btn-outline {
     467        background: transparent;
     468        border: 1px solid #e2e8f0;
     469        color: #1e293b;
     470
     471        &:hover {
     472            background: #f1f5f9;
     473        }
     474    }
     475
     476    .btn-danger {
     477        background: #ef4444;
     478        color: #fff;
     479    }
     480
     481    table {
     482        width: 100%;
     483        border-collapse: collapse;
     484        margin-top: 1rem;
     485    }
     486
     487    th {
     488        text-align: left;
     489        padding: 12px;
     490        font-size: 14px;
     491        color: #64748b;
     492
     493        &:last-child {
     494            text-align: center;
     495        }
     496    }
     497
     498    td {
     499        padding: 12px;
     500        border-bottom: 1px solid #c3c4c7;
     501        font-size: 14px;
     502    }
     503
     504    tr:last-child {
     505        td {
     506            border-bottom: none;
     507        }
     508    }
     509
     510    .tag {
     511        padding: 2px 8px;
     512        border-radius: 4px;
     513        font-size: 11px;
     514        font-weight: bold;
     515        text-transform: uppercase;
     516    }
     517
     518    .tag-unused {
     519        background: #fee2e2;
     520        color: #991b1b;
     521    }
     522
     523    .tag-duplicate {
     524        background: #fef3c7;
     525        color: #92400e;
     526    }
     527
     528    .tab-content {
     529        display: none;
     530
     531        &.active {
     532            display: block;
     533        }
     534    }
     535
     536    .page-subtitle {
     537        font-size: 13px;
     538        color: #64748b;
     539        margin-top: 10px;
     540        margin-bottom: 0;
     541    }
     542
     543    .stacked {
     544        display: flex;
     545        flex-direction: column;
     546        gap: 10px;
     547        margin-top: 10px;
     548    }
     549
     550    .inline-actions {
     551        display: flex;
     552        gap: 10px;
     553        align-items: center;
     554        flex-wrap: wrap;
     555        justify-content: flex-end;
     556    }
     557
     558    .modal-backdrop {
     559        position: fixed;
     560        inset: 0;
     561        background: rgba(15, 23, 42, .55);
     562        display: none;
     563        align-items: center;
     564        justify-content: center;
     565        z-index: 50;
     566
     567        &.active {
     568            display: flex;
     569        }
     570    }
     571
     572    .modal {
     573        background: #ffffff;
     574        border-radius: 12px;
     575        padding: 1.5rem;
     576        width: 480px;
     577        max-width: 94%;
     578        box-shadow: 0 20px 40px rgba(15, 23, 42, .3);
     579        border: 1px solid #e2e8f0;
     580    }
     581
     582    .modal-header {
     583        display: flex;
     584        align-items: center;
     585        justify-content: space-between;
     586        margin-bottom: 1rem;
     587    }
     588
     589    .modal-title {
     590        font-size: 18px;
     591        font-weight: 600;
     592        display: flex;
     593        align-items: center;
     594        gap: 8px;
     595    }
     596
     597    .modal-close {
     598        border: none;
     599        background: transparent;
     600        cursor: pointer;
     601        font-size: 18px;
     602        color: #64748b;
     603    }
     604
     605    .modal-body {
     606        display: flex;
     607        flex-direction: column;
     608        gap: 12px;
     609        margin-bottom: 1.5rem;
     610
     611        label {
     612            font-size: 13px;
     613            margin-bottom: 4px;
     614            display: block;
     615        }
     616
     617        input, select {
     618            width: 100%;
     619            padding: 8px 10px;
     620            border-radius: 6px;
     621            border: 1px solid #e2e8f0;
     622            font-size: 13px;
     623
     624            &:focus {
     625                outline: none;
     626                border-color: #6366f1;
     627                box-shadow: 0 0 0 1px rgba(99, 102, 241, .25);
     628            }
     629        }
     630    }
     631
     632    .modal-hint {
     633        font-size: 11px;
     634        color: #64748b;
     635    }
     636
     637    .modal-footer {
     638        display: flex;
     639        justify-content: flex-end;
     640        gap: 10px;
     641    }
     642
     643    /* Nested Components from top-level */
     644
     645    .mediatracker-usage-table {
    192646        margin-top: 15px;
    193         margin-bottom: 10px;
    194 
    195         svg {
    196             width: 40px;
    197             height: 40px;
    198         }
    199     }
    200 
    201     .wp-filter {
    202         margin: 10px 0 10px;
    203     }
    204 
    205     .notice {
     647
     648        table.wp-list-table {
     649            th, td {
     650                vertical-align: middle;
     651            }
     652
     653            .button {
     654                margin-right: 4px;
     655            }
     656        }
     657    }
     658
     659    // Unused Media List CSS
     660    .unused-media-list {
     661        .wp-list-table {
     662            td strong {
     663                display: block;
     664                margin-bottom: .2em;
     665                font-size: 14px;
     666            }
     667
     668            .media-icon {
     669                float: left;
     670                min-height: 60px;
     671                margin: 0 9px 0 0;
     672
     673                img {
     674                    width: 60px;
     675                    height: 60px;
     676                }
     677            }
     678
     679            &.fixed {
     680                table-layout: inherit;
     681            }
     682        }
     683
     684        .search-box {
     685            margin: 0px;
     686        }
     687
     688        .wp-filter {
     689            margin: 0 0 20px;
     690        }
     691
     692        .notice {
     693            h2 {
     694                margin-bottom: 0;
     695            }
     696        }
     697
     698        /* Simple styling for the admin table */
     699        table.widefat {
     700            table-layout: inherit;
     701            width: 100%;
     702            margin-top: 20px;
     703
     704            th,
     705            td {
     706                padding: 10px;
     707                text-align: left;
     708
     709                &.status {
     710                    font-weight: 700;
     711                    color: red;
     712                }
     713            }
     714        }
     715    }
     716
     717    .media-toolbar-wrap {
     718        &.wp-filter {
     719            display: flex;
     720            justify-content: space-between;
     721            align-items: center;
     722            gap: 20px;
     723            border: none;
     724            box-shadow: none;
     725
     726            .search-form {
     727                input[type=search] {
     728                    width: 215px;
     729                }
     730            }
     731        }
     732    }
     733
     734    .unused-image-found {
    206735        h2 {
    207             margin-bottom: 0;
    208         }
    209     }
    210 
    211     /* Simple styling for the admin table */
    212     table.widefat {
    213         table-layout: inherit;
     736            font-size: 20px;
     737            font-weight: 400;
     738
     739            span {
     740                font-size: 20px;
     741                font-weight: bold;
     742                color: #6366f1;
     743            }
     744        }
     745    }
     746
     747    .replace-broken-link {
     748        input {
     749            width: 100%;
     750        }
     751    }
     752
     753    #clear-broken-links-transient {
     754        position: absolute;
     755        padding: 8px 30px;
     756        font-size: 14px;
     757        font-weight: 500;
     758    }
     759
     760    #success-message {
     761        display: none;
     762        color: green;
     763        margin-top: 15px;
     764        position: absolute;
     765        left: 230px;
     766        font-size: 16px;
     767    }
     768
     769    .wp-list-table {
     770        #usage_count {
     771            width: 130px;
     772        }
     773    }
     774
     775    // Duplicate Form & Components
     776    #mt-duplicate-form {
     777        table {
     778            tbody td:last-child,
     779            tr th:last-child {
     780                text-align: center;
     781            }
     782
     783            .check-column {
     784                background: transparent;
     785                border-bottom: 1px solid #c3c4c7;
     786                width: 3.2em;
     787                padding: 16px 3px;
     788            }
     789        }
     790
     791        .duplicate-media-footer {
     792            display: flex;
     793            align-items: center;
     794            gap: 30px;
     795            justify-content: space-between;
     796            margin-top: 15px;
     797        }
     798
     799        .tablenav-pages {
     800            .paging-input {
     801                margin: 0 15px;
     802            }
     803        }
     804    }
     805
     806    .duplicate-media-count {
     807        h2 {
     808            font-size: 20px;
     809            color: #6366f1;
     810            font-weight: 700;
     811
     812            span {
     813                color: #1d2327;
     814                font-weight: 400;
     815            }
     816        }
     817    }
     818
     819    .media-header {
     820        display: flex;
     821        justify-content: space-between;
     822        align-items: center;
     823        margin-bottom: 30px;
     824    }
     825
     826    #tab-unused-media {
     827        .tablenav.bottom {
     828            margin-top: 15px;
     829        }
     830    }
     831
     832    .mt-overview-table {
     833        border-radius: 8px;
     834        overflow: hidden;
     835        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
     836        margin-top: 20px;
    214837        width: 100%;
    215         margin-top: 20px;
    216     }
    217 
    218     table.widefat th,
    219     table.widefat td {
    220         padding: 10px;
    221         text-align: left;
    222 
    223         &.status {
    224             font-weight: 700;
    225             color: red;
    226         }
    227     }
    228 }
    229 
    230 .media-toolbar-wrap.wp-filter {
     838        border-collapse: separate;
     839        border-spacing: 0;
     840        background: #fff;
     841
     842        thead {
     843            th {
     844                background-color: #f8fafc;
     845                color: #475569;
     846                font-weight: 600;
     847                text-transform: uppercase;
     848                font-size: 11px;
     849                letter-spacing: 0.05em;
     850                padding: 16px;
     851                border-bottom: 1px solid #e2e8f0;
     852                text-align: left;
     853            }
     854
     855            tr:first-child {
     856                th:first-child { border-top-left-radius: 8px; }
     857                th:last-child { border-top-right-radius: 8px; }
     858            }
     859        }
     860
     861        tbody {
     862            tr {
     863                transition: background-color 0.2s;
     864
     865                &:nth-child(even) {
     866                    background-color: #f8fafc;
     867                }
     868
     869                &:hover {
     870                    background-color: #f1f5f9;
     871                }
     872
     873                &:last-child td {
     874                    border-bottom: none;
     875                }
     876            }
     877
     878            td {
     879                padding: 16px;
     880                color: #334155;
     881                vertical-align: middle;
     882                border-bottom: 1px solid #f1f5f9;
     883                font-size: 14px;
     884
     885                &:last-child {
     886                    text-align: center;
     887                }
     888
     889                strong {
     890                    color: #0f172a;
     891                    font-weight: 500;
     892                }
     893
     894                small {
     895                    color: #94a3b8;
     896                    display: block;
     897                    margin-top: 4px;
     898                }
     899            }
     900
     901            tr:last-child {
     902                td:first-child { border-bottom-left-radius: 8px; }
     903                td:last-child { border-bottom-right-radius: 8px; }
     904            }
     905        }
     906    }
     907
     908    // Helper Utility Classes
     909    .mt-flex-col-end { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
     910    .mt-flex-center { display: flex; gap: 10px; align-items: center; }
     911    .mt-stat-title { display: flex; align-items: center; gap: 8px; }
     912    .mt-icon-indigo { color: #6366f1; }
     913    .mt-icon-amber { color: #f59e0b; }
     914    .mt-stat-subtitle { color: #64748b; font-size: 12px; }
     915    .mt-mt-10 { margin-top: 10px; }
     916    .mt-helper-text { font-size: 12px; color: #64748b; }
     917    .mt-btn-sm { padding: 6px 12px !important; font-size: 12px !important; }
     918    .mt-btn-xs { padding: 5px 10px !important; }
     919    .mt-mime-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8fafc; border-radius: 8px; }
     920    .mt-mime-icon { color: #6366f1; width: 20px; text-align: center; }
     921    .mt-flex-1 { flex: 1; }
     922    .mt-font-medium { font-size: 14px; font-weight: 500; }
     923    .mt-empty-state { color: #64748b; font-size: 12px; text-align: center; padding: 20px; }
     924    .mt-mt-6 { margin-top: 1.5rem; }
     925    .mt-empty-state-large { color: #64748b; text-align: center; padding: 40px; }
     926    .mt-success-icon-large { font-size: 48px; color: #10b981; margin-bottom: 15px; height: 48px; width: 48px; }
     927    .mt-tag-success { background: #10b981; color: white; }
     928    .mt-btn-xs-clean { padding: 5px 10px !important; text-decoration: none; }
     929    .mt-chart-icon-large { font-size: 48px; color: #cbd5e1; margin-bottom: 15px; height: 48px; width: 48px; }
     930    .mt-mb-3 { margin-bottom: 12px; }
     931    .mt-progress-text { margin-left: 8px; font-size: 11px; color: #64748b; min-width: 40px; text-align: right; display: none; }
     932    .mt-status-text { margin-left: 4px; display: none; }
     933    .mt-progress-track { flex: 1; max-width: 100%; height: 15px; background: linear-gradient(90deg, #eef2ff, #e2e8f0); border-radius: 999px; overflow: hidden; display: none; box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.4); }
     934    .mt-progress-bar-fill { width: 0%; height: 100%; background: #6366f1; }
     935    .mt-v-middle { vertical-align: middle; }
     936    .mt-text-center { text-align: center; }
     937    .mt-mr-1 { margin-right: 4px; }
     938    .mt-thumb-img { width: 60px; height: auto; }
     939    .mt-link-clean { text-decoration: none; font-weight: 500; }
     940
     941    @media (max-width: 960px) {
     942        flex-direction: column;
     943
     944        aside {
     945            width: 100%;
     946            flex-direction: row;
     947            align-items: center;
     948            justify-content: space-between;
     949            padding: 1rem 1.5rem;
     950        }
     951
     952        nav ul {
     953            display: flex;
     954            overflow-x: auto;
     955        }
     956
     957        nav li {
     958            white-space: nowrap;
     959        }
     960    }
     961}
     962
     963/* Featured Video Card */
     964.media-video-featured-card {
     965    background: #fff;
     966    border: 1px solid #e2e8f0;
     967    border-radius: 12px;
     968    overflow: hidden;
     969    display: flex;
     970    flex-direction: column; /* Side by side layout */
     971    align-items: stretch;
     972    transition: all 0.3s ease;
     973    cursor: pointer;
     974    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, .05);
     975    width: 32%;
     976
     977    &:hover {
     978        transform: translateY(-2px);
     979        box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
     980        border-color: #cbd5e1;
     981
     982        .video-thumbnail {
     983            opacity: 0.6;
     984        }
     985
     986        .play-icon {
     987            transform: scale(1.1);
     988            background: #fff;
     989            box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.3);
     990        }
     991    }
     992
     993    .video-thumbnail-wrapper {
     994        position: relative;
     995        background: #000;
     996        overflow: hidden;
     997        display: flex;
     998        align-items: center;
     999        justify-content: center;
     1000    }
     1001
     1002    .video-thumbnail {
     1003        width: 100%;
     1004        height: 100%;
     1005        object-fit: cover;
     1006        opacity: 0.8;
     1007        transition: opacity 0.3s ease;
     1008    }
     1009
     1010    .play-button-overlay {
     1011        position: absolute;
     1012        top: 0;
     1013        left: 0;
     1014        width: 100%;
     1015        height: 100%;
     1016        display: flex;
     1017        align-items: center;
     1018        justify-content: center;
     1019        z-index: 2;
     1020    }
     1021
     1022    .play-icon {
     1023        width: 60px;
     1024        height: 60px;
     1025        background: rgba(255, 255, 255, 0.9);
     1026        border-radius: 50%;
     1027        display: flex;
     1028        align-items: center;
     1029        justify-content: center;
     1030        transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
     1031        box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
     1032
     1033        .dashicons {
     1034            font-size: 32px;
     1035            width: 32px;
     1036            height: 32px;
     1037            color: #6366f1; /* Primary color */
     1038            margin-left: 4px; /* Visual adjustment */
     1039        }
     1040    }
     1041
     1042    .video-card-content {
     1043        padding: 2rem;
     1044        flex: 1;
     1045        display: flex;
     1046        flex-direction: column;
     1047        justify-content: center;
     1048        align-items: flex-start;
     1049
     1050        h4 {
     1051            margin: 0 0 0.75rem 0;
     1052            font-size: 1.25rem;
     1053            color: #1e293b;
     1054            font-weight: 600;
     1055        }
     1056    }
     1057}
     1058
     1059/* Modal Styles */
     1060.mt-video-modal-backdrop {
     1061    position: fixed;
     1062    inset: 0;
     1063    background: rgba(15, 23, 42, 0.75);
     1064    z-index: 100000; /* High z-index */
     1065    display: none;
     1066    align-items: center;
     1067    justify-content: center;
     1068    backdrop-filter: blur(4px);
     1069    opacity: 0;
     1070    transition: opacity 0.3s ease;
     1071
     1072    &.active {
     1073        display: flex;
     1074        opacity: 1;
     1075
     1076        .mt-video-modal {
     1077            transform: scale(1);
     1078        }
     1079    }
     1080}
     1081
     1082.mt-video-modal {
     1083    background: #fff;
     1084    width: 90%;
     1085    max-width: 800px;
     1086    border-radius: 16px;
     1087    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
     1088    overflow: hidden;
     1089    transform: scale(0.95);
     1090    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
     1091}
     1092
     1093.mt-video-modal-header {
     1094    padding: 1rem 1.5rem;
     1095    border-bottom: 1px solid #e2e8f0;
    2311096    display: flex;
    2321097    justify-content: space-between;
    2331098    align-items: center;
    234     gap: 20px;
    235     padding: 0 15px;
    236 
    237     .search-form {
    238         input[type=search] {
    239             width: 215px;
    240         }
    241     }
    242 }
    243 
    244 .unused-image-found h2 {
    245     font-size: 24px;
    246     font-weight: 300;
    247 
    248     span {
    249         font-size: 28px;
    250         font-weight: bold;
    251         color: #cf0000;
    252     }
    253 }
    254 
    255 .replace-broken-link {
    256     input {
     1099    background: #f8fafc;
     1100
     1101    h3 {
     1102        margin: 0;
     1103        font-size: 1.1rem;
     1104        color: #334155;
     1105    }
     1106}
     1107
     1108.mt-video-modal-close {
     1109    background: transparent;
     1110    border: none;
     1111    cursor: pointer;
     1112    color: #64748b;
     1113    padding: 4px;
     1114    border-radius: 4px;
     1115    display: flex;
     1116    align-items: center;
     1117    justify-content: center;
     1118    transition: background 0.2s;
     1119
     1120    &:hover {
     1121        background: #e2e8f0;
     1122        color: #ef4444;
     1123    }
     1124}
     1125
     1126.mt-video-modal-body {
     1127    padding: 0;
     1128    background: #000;
     1129}
     1130
     1131.mt-responsive-video-wrapper {
     1132    position: relative;
     1133    padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
     1134    height: 0;
     1135    overflow: hidden;
     1136
     1137    iframe {
     1138        position: absolute;
     1139        top: 0;
     1140        left: 0;
    2571141        width: 100%;
    258     }
    259 }
    260 
    261 #clear-broken-links-transient {
    262     position: absolute;
    263     padding: 8px 30px;
    264     font-size: 14px;
    265     font-weight: 500;
    266 }
    267 
    268 #success-message {
    269     display: none;
    270     color: green;
    271     margin-top: 15px;
    272     position: absolute;
    273     left: 230px;
    274     font-size: 16px;
    275 }
    276 
    277 .wp-list-table {
    278     #usage_count {
    279         width: 130px;
    280     }
    281 }
     1142        height: 100%;
     1143        border: 0;
     1144    }
     1145}
     1146
     1147/* Responsive adjustments */
     1148@media (max-width: 768px) {
     1149    .media-video-featured-card {
     1150        flex-direction: column;
     1151
     1152        .video-thumbnail-wrapper {
     1153            width: 100%;
     1154            height: 200px;
     1155        }
     1156
     1157        .video-card-content {
     1158            padding: 1.5rem;
     1159        }
     1160    }
     1161}
  • media-tracker/trunk/composer.json

    r3151282 r3454648  
    1515            "Media_Tracker\\": "includes/"
    1616        },
    17         "files": []
     17        "files": [
     18            "includes/functions.php"
     19        ]
    1820    }
    1921}
  • media-tracker/trunk/includes/Admin.php

    r3432010 r3454648  
    1717        new Admin\Media_Usage();
    1818        new Admin\Duplicate_Images();
     19        new Admin\PluginMeta();
    1920    }
    2021}
  • media-tracker/trunk/includes/Admin/Duplicate_Images.php

    r3432010 r3454648  
    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 
    27         // Add faster cron schedule (5 minutes) for quicker initial hashing
    28         add_filter('cron_schedules', function($schedules) {
    29             if (!isset($schedules['five_minutes'])) {
    30                 $schedules['five_minutes'] = array(
    31                     'interval' => 5 * 60,
    32                     'display'  => __('Every 5 Minutes', 'media-tracker'),
    33                 );
    34             }
    35             return $schedules;
    36         });
    37 
    38         // Schedule the cron job for batch processing
    39         if (!wp_next_scheduled('media_tracker_batch_process')) {
    40             // Prefer the faster schedule when available
    41             $interval = 'five_minutes';
    42             $schedules = wp_get_schedules();
    43             if (!isset($schedules[$interval])) {
    44                 $interval = 'hourly';
    45             }
    46             wp_schedule_event(time(), $interval, 'media_tracker_batch_process');
    47         }
     26        add_action('wp_ajax_mt_delete_duplicate_images', array($this, 'delete_duplicate_images_via_ajax'));
    4827    }
    4928
     
    9473
    9574            // If no duplicates yet and many attachments lack hashes, trigger a quick batch
     75            // Manual scan only: disable auto-trigger
     76            /*
    9677            if (empty($hashes) && $this->count_unhashed_attachments() > 0) {
    9778                // Run one batch immediately to bootstrap hashes; then re-check
     
    9980                $hashes = $this->get_duplicate_hashes_by_sql();
    10081            }
     82            */
    10183
    10284            $duplicate_ids = array();
     
    358340     */
    359341    public function process_image_hashes_batch() {
     342        // Manual scan only: check if scan is active
     343        if ( ! get_option( 'media_tracker_duplicate_scan_active' ) ) {
     344            return;
     345        }
     346
    360347        $limit = MEDIA_TRACKER_DUPLICATE_BATCH_SIZE;
    361348        $offset = get_option('media_tracker_offset', 0);
     
    367354            // If no more images, reset the offset
    368355            delete_option('media_tracker_offset');
     356            // Scan complete: disable active flag
     357            delete_option( 'media_tracker_duplicate_scan_active' );
     358
     359            // Save final duplicate count
     360            $count = self::count_duplicate_attachments();
     361            update_option( 'media_tracker_duplicate_count_last_scan', $count );
     362
     363            // Invalidate dashboard stats cache
     364            delete_transient( 'media_tracker_dashboard_stats_v8' );
    369365        } else {
    370366            // Update the offset for the next batch
     
    386382        $hashes = $this->get_duplicate_hashes_by_sql();
    387383        // If no duplicates yet and many attachments lack hashes, trigger a quick batch
     384        // Manual scan only: disable auto-trigger
     385        /*
    388386        if (empty($hashes) && $this->count_unhashed_attachments() > 0) {
    389387            do_action('media_tracker_batch_process');
    390388            $hashes = $this->get_duplicate_hashes_by_sql();
    391389        }
     390        */
    392391        $duplicate_ids = array();
    393392        $ids_with_hashes = array();
     
    434433            wp_send_json_error();
    435434        }
     435    }
     436
     437    /**
     438     * Get the total count of duplicate images.
     439     *
     440     * @return int Total number of duplicate images.
     441     */
     442    public static function count_duplicate_attachments() {
     443        global $wpdb;
     444
     445        $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     446            $wpdb->prepare(
     447                "SELECT COUNT(*) as count
     448                FROM {$wpdb->postmeta} pm
     449                INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
     450                WHERE pm.meta_key = %s
     451                AND p.post_type = 'attachment'
     452                AND p.post_mime_type LIKE %s
     453                AND pm.meta_value != ''
     454                GROUP BY pm.meta_value
     455                HAVING count > 1",
     456                '_media_tracker_hash',
     457                'image/%'
     458            )
     459        );
     460
     461        $count = 0;
     462        if ( $results ) {
     463            foreach ( $results as $row ) {
     464                $count += $row->count;
     465            }
     466        }
     467
     468        return $count;
    436469    }
    437470
     
    517550    // Add a new helper that aggregates hashes across the entire library
    518551    private function get_all_duplicate_hashes($batch_size = 300) {
    519         global $wpdb;
    520 
    521         $offset = 0;
    522         $hashes = array();
    523         $iterations = 0;
    524         $max_iterations = 10000; // Safety limit
    525 
    526         while ($iterations < $max_iterations) {
    527             $attachments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    528                 $wpdb->prepare(
    529                     "SELECT ID, guid FROM {$wpdb->posts}
    530                     WHERE post_type = %s AND post_mime_type LIKE %s
    531                     LIMIT %d, %d",
    532                     'attachment',
    533                     'image/%',
    534                     $offset,
    535                     $batch_size
    536                 )
    537             );
    538 
    539             if (empty($attachments)) {
    540                 break;
    541             }
    542 
    543             foreach ($attachments as $attachment) {
    544                 $file_path = get_attached_file($attachment->ID);
    545 
    546                 if ( $file_path && file_exists($file_path) ) {
    547                     $hash = get_post_meta($attachment->ID, '_media_tracker_hash', true);
    548 
    549                     // Get mime for deciding hashing strategy
    550                     $mime = get_post_mime_type($attachment->ID);
    551 
    552                     if (empty($hash)) {
    553                         try {
    554                             $hash = $this->generate_image_hash($file_path, $mime);
    555                             update_post_meta($attachment->ID, '_media_tracker_hash', $hash);
    556                         } catch (Exception $e) {
    557                             // Silently skip images that can't be hashed
    558                             continue;
    559                         }
    560                     }
    561 
    562                     if (!isset($hashes[$hash])) {
    563                         $hashes[$hash] = array();
    564                     }
    565                     $hashes[$hash][] = $attachment->ID;
    566                 }
    567             }
    568 
    569             $offset += $batch_size;
    570             $iterations++;
    571 
    572             if (count($attachments) < $batch_size) {
    573                 break;
    574             }
    575         }
    576 
    577         // Only return hashes with duplicates across the entire set
    578         return array_filter($hashes, function($ids) {
    579             return count($ids) > 1;
    580         });
     552        // Strictly use existing database hashes.
     553        // Never generate new hashes here. Hash generation is now exclusively handled
     554        // by the manual scan process (reset_duplicate_hashes_via_ajax -> batch process).
     555        return $this->get_duplicate_hashes_by_sql();
    581556    }
    582557
     
    604579        // Reset the offset
    605580        delete_option('media_tracker_offset');
     581
     582        // Activate manual scan flag
     583        update_option( 'media_tracker_duplicate_scan_active', true );
     584
     585        // Mark that a scan has been initiated/performed
     586        update_option( 'media_tracker_duplicates_scanned', true );
     587
     588        // Clear dashboard stats cache so overview updates
     589        delete_transient( 'media_tracker_dashboard_stats_v8' );
    606590
    607591        // Trigger immediate batch processing
     
    622606        ));
    623607    }
     608
     609    public function delete_duplicate_images_via_ajax() {
     610        if ( ! current_user_can( 'manage_options' ) ) {
     611            wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 );
     612        }
     613
     614        check_ajax_referer( 'media_tracker_nonce', 'nonce' );
     615
     616        if ( empty( $_POST['attachment_ids'] ) || ! is_array( $_POST['attachment_ids'] ) ) {
     617            wp_send_json_error( array( 'message' => __( 'No images selected.', 'media-tracker' ) ) );
     618        }
     619
     620        $ids = array_map( 'intval', $_POST['attachment_ids'] );
     621        $deleted = 0;
     622
     623        foreach ( $ids as $id ) {
     624            if ( $id <= 0 ) {
     625                continue;
     626            }
     627            if ( ! current_user_can( 'delete_post', $id ) ) {
     628                continue;
     629            }
     630            $result = wp_delete_attachment( $id, true );
     631            if ( $result ) {
     632                $deleted++;
     633            }
     634        }
     635
     636        if ( $deleted > 0 ) {
     637            // Clear dashboard stats cache so overview updates
     638            delete_transient( 'media_tracker_dashboard_stats_v6' );
     639
     640            wp_send_json_success( array(
     641                'message' => sprintf(
     642                    /* translators: %d: number of deleted images */
     643                    __( 'Deleted %d duplicate images.', 'media-tracker' ),
     644                    $deleted
     645                ),
     646                'deleted' => $deleted,
     647            ) );
     648        } else {
     649            wp_send_json_error( array( 'message' => __( 'No images were deleted.', 'media-tracker' ) ) );
     650        }
     651    }
     652
     653    /**
     654     * Check if any media tracker hash exists in the database.
     655     *
     656     * @return bool True if hash exists, false otherwise.
     657     */
     658    public static function has_generated_hashes() {
     659        global $wpdb;
     660        $hash_exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     661            $wpdb->prepare(
     662                "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT 1",
     663                '_media_tracker_hash'
     664            )
     665        );
     666        return ! empty( $hash_exists );
     667    }
     668
     669    /**
     670     * Render pagination for duplicate images tab.
     671     *
     672     * @param int $current_page Current page number.
     673     * @param int $total_pages  Total number of pages.
     674     * @param int $total_items  Total number of items.
     675     */
     676    public static function render_pagination( $current_page, $total_pages, $total_items ) {
     677        if ( $total_pages <= 1 ) {
     678            return;
     679        }
     680
     681        $base_url = add_query_arg( 'tab', 'duplicates', remove_query_arg( array( 'mt_dup_page' ) ) );
     682
     683        echo '<div class="tablenav"><div class="tablenav-pages">';
     684        echo '<span class="displaying-num">' . esc_html( $total_items ) . ' ' . esc_html__( 'items', 'media-tracker' ) . '</span>';
     685
     686        if ( $current_page > 1 ) {
     687            $prev_url = add_query_arg( 'mt_dup_page', $current_page - 1, $base_url );
     688            echo '<a class="prev-page button" href="' . esc_url( $prev_url ) . '">&laquo;</a>';
     689        } else {
     690            echo '<span class="tablenav-pages-navspan">&laquo;</span>';
     691        }
     692
     693        echo '<span class="paging-input">' . esc_html( $current_page ) . ' / ' . esc_html( $total_pages ) . '</span>';
     694
     695        if ( $current_page < $total_pages ) {
     696            $next_url = add_query_arg( 'mt_dup_page', $current_page + 1, $base_url );
     697            echo '<a class="next-page button" href="' . esc_url( $next_url ) . '">&raquo;</a>';
     698        } else {
     699            echo '<span class="tablenav-pages-navspan">&raquo;</span>';
     700        }
     701
     702        echo '</div></div>';
     703    }
    624704}
  • media-tracker/trunk/includes/Admin/Media_Usage.php

    r3432010 r3454648  
    2727
    2828    /**
     29     * Get most used media for dashboard stats.
     30     *
     31     * @return array Array of objects with ID, post_title, post_mime_type, usage_count.
     32     */
     33    public static function get_dashboard_most_used_media() {
     34        global $wpdb;
     35
     36        $media_tracker_most_used = array();
     37        $media_tracker_all_usage = array();
     38
     39        // Source 1: Featured images (_thumbnail_id).
     40        $media_tracker_featured_media = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     41            "SELECT p.ID, p.post_title, p.post_mime_type, COUNT(pm.meta_value) as usage_count
     42            FROM {$wpdb->posts} p
     43            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.meta_value
     44            WHERE pm.meta_key = '_thumbnail_id'
     45            AND p.post_type = 'attachment'
     46            AND p.post_status = 'inherit'
     47            GROUP BY p.ID"
     48        );
     49
     50        foreach ( $media_tracker_featured_media as $media_tracker_item ) {
     51            $media_tracker_id = $media_tracker_item->ID;
     52            if ( ! isset( $media_tracker_all_usage[ $media_tracker_id ] ) ) {
     53                $media_tracker_all_usage[ $media_tracker_id ] = array(
     54                    'ID'             => $media_tracker_id,
     55                    'post_title'     => $media_tracker_item->post_title,
     56                    'post_mime_type' => $media_tracker_item->post_mime_type,
     57                    'usage_count'    => 0,
     58                );
     59            }
     60            $media_tracker_all_usage[ $media_tracker_id ]['usage_count'] += $media_tracker_item->usage_count;
     61        }
     62
     63        // Source 2: Content usage - count ALL occurrences in post content.
     64        $media_tracker_all_attachments = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     65            "SELECT ID FROM {$wpdb->posts}
     66            WHERE post_type = 'attachment'
     67            AND post_status = 'inherit'"
     68        );
     69
     70        foreach ( $media_tracker_all_attachments as $media_tracker_attachment_id ) {
     71            // Count in post content (wp-image-XXXX class).
     72            $media_tracker_wp_image_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     73                $wpdb->prepare(
     74                    "SELECT COUNT(DISTINCT ID)
     75                    FROM {$wpdb->posts}
     76                    WHERE post_status = 'publish'
     77                    AND post_content LIKE %s",
     78                    '%wp-image-' . intval( $media_tracker_attachment_id ) . '%'
     79                )
     80            );
     81
     82            // Count in post content (Gutenberg blocks).
     83            $media_tracker_gutenberg_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     84                $wpdb->prepare(
     85                    "SELECT COUNT(DISTINCT ID)
     86                    FROM {$wpdb->posts}
     87                    WHERE post_status = 'publish'
     88                    AND post_content LIKE %s",
     89                    '%"id":' . intval( $media_tracker_attachment_id ) . ',%'
     90                )
     91            );
     92
     93            // Count in post content (direct URLs).
     94            $media_tracker_attachment_url = wp_get_attachment_url( $media_tracker_attachment_id );
     95            $media_tracker_url_count      = 0;
     96            if ( $media_tracker_attachment_url ) {
     97                $media_tracker_url_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     98                    $wpdb->prepare(
     99                        "SELECT COUNT(DISTINCT ID)
     100                        FROM {$wpdb->posts}
     101                        WHERE post_status = 'publish'
     102                        AND post_content LIKE %s",
     103                        '%' . $wpdb->esc_like( $media_tracker_attachment_url ) . '%'
     104                    )
     105                );
     106            }
     107
     108            $media_tracker_total_usage = intval( $media_tracker_wp_image_count ) + intval( $media_tracker_gutenberg_count ) + intval( $media_tracker_url_count );
     109
     110            if ( $media_tracker_total_usage > 0 ) {
     111                if ( ! isset( $media_tracker_all_usage[ $media_tracker_attachment_id ] ) ) {
     112                    $media_tracker_post                      = get_post( $media_tracker_attachment_id );
     113                    $media_tracker_all_usage[ $media_tracker_attachment_id ] = array(
     114                        'ID'             => $media_tracker_attachment_id,
     115                        'post_title'     => $media_tracker_post->post_title,
     116                        'post_mime_type' => $media_tracker_post->post_mime_type,
     117                        'usage_count'    => 0,
     118                    );
     119                }
     120                $media_tracker_all_usage[ $media_tracker_attachment_id ]['usage_count'] += $media_tracker_total_usage;
     121            }
     122        }
     123
     124        // Convert to object array and sort by usage.
     125        foreach ( $media_tracker_all_usage as $media_tracker_usage ) {
     126            $media_tracker_obj                 = new \stdClass();
     127            $media_tracker_obj->ID             = $media_tracker_usage['ID'];
     128            $media_tracker_obj->post_title     = $media_tracker_usage['post_title'];
     129            $media_tracker_obj->post_mime_type = $media_tracker_usage['post_mime_type'];
     130            $media_tracker_obj->usage_count    = $media_tracker_usage['usage_count'];
     131            $media_tracker_most_used[]         = $media_tracker_obj;
     132        }
     133
     134        // Sort by usage count descending.
     135        usort(
     136            $media_tracker_most_used,
     137            function( $a, $b ) {
     138                return $b->usage_count - $a->usage_count;
     139            }
     140        );
     141
     142        // Get top 5.
     143        return array_slice( $media_tracker_most_used, 0, 5 );
     144    }
     145
     146    /**
     147     * Get media type statistics for dashboard.
     148     *
     149     * @return array Array of objects with post_mime_type and count.
     150     */
     151    public static function get_mime_type_stats() {
     152        global $wpdb;
     153
     154        return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     155            $wpdb->prepare(
     156                "SELECT post_mime_type, COUNT(*) as count
     157                FROM {$wpdb->posts}
     158                WHERE post_type = %s
     159                AND post_status = %s
     160                AND post_mime_type != ''
     161                GROUP BY post_mime_type
     162                ORDER BY count DESC
     163                LIMIT 5",
     164                'attachment',
     165                'inherit'
     166            )
     167        );
     168    }
     169
     170    /**
    29171     * Add a custom meta box to the media file details page
    30172     *
     
    167309        if ( $results ) {
    168310            echo '
    169             <table>
     311            <table class="wp-list-table widefat fixed striped table-view-list">
    170312                <thead>
    171313                    <tr>
    172                         <th>' . esc_html__( '#', 'media-tracker' ) . '</th>
    173                         <th>' . esc_html__( 'Title', 'media-tracker' ) . '</th>
    174                         <th>' . esc_html__( 'Type', 'media-tracker' ) . '</th>
    175                         <th>' . esc_html__( 'Date Added', 'media-tracker' ) . '</th>
    176                         <th>' . esc_html__( 'Actions', 'media-tracker' ) . '</th>
     314                        <th scope="col" class="manage-column column-primary" style="width: 50px;">' . esc_html__( '#', 'media-tracker' ) . '</th>
     315                        <th scope="col" class="manage-column">' . esc_html__( 'Title', 'media-tracker' ) . '</th>
     316                        <th scope="col" class="manage-column">' . esc_html__( 'Type', 'media-tracker' ) . '</th>
     317                        <th scope="col" class="manage-column">' . esc_html__( 'Date Added', 'media-tracker' ) . '</th>
     318                        <th scope="col" class="manage-column">' . esc_html__( 'Actions', 'media-tracker' ) . '</th>
    177319                    </tr>
    178320                </thead>
     
    278420
    279421        // Posts using this attachment as featured image
    280         $thumbnail_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     422        $thumbnail_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    281423            $wpdb->prepare(
    282424                "
     
    295437        // ACF usage
    296438        if(class_exists('ACF')) {
    297             $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     439            $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    298440                $wpdb->prepare(
    299441                    "
     
    319461            $results = array_merge($results, $acf_posts);
    320462
    321             $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     463            $acf_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    322464                $wpdb->prepare(
    323465                    "
     
    341483
    342484        // Elementor usage
    343         $elementor_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     485        $elementor_posts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    344486            $wpdb->prepare(
    345487                "
  • media-tracker/trunk/includes/Admin/Menu.php

    r3432010 r3454648  
    1818     */
    1919    public function __construct() {
    20         add_action( 'admin_menu', array( $this, 'register_unused_media_cleaner_menu' ) );
    21         add_filter( 'set-screen-option', array( $this, 'set_screen_option' ), 10, 3 );
    22         add_action( 'load-media_page_unused-media-cleaner', array( $this, 'add_screen_options' ) );
    23         // Suppress admin notices on our screen before header renders
    24         add_action( 'load-media_page_unused-media-cleaner', array( $this, 'suppress_admin_notices' ) );
     20        add_action( 'admin_menu', array( $this, 'register_media_tracker_menu' ) );
    2521
    2622        // Handle AJAX: clear plugin-related transients/cache
     
    3228        // Handle AJAX: run media scan in background (start)
    3329        add_action( 'wp_ajax_run_media_scan', array( $this, 'handle_run_media_scan' ) );
     30
    3431        // NEW: Handle AJAX: run media scan synchronously (fallback if cron stalls)
    3532        add_action( 'wp_ajax_run_media_scan_sync', array( $this, 'handle_run_media_scan_sync' ) );
     
    4340        // Handle AJAX: get unused media count
    4441        add_action( 'wp_ajax_get_unused_media_count', array( $this, 'handle_get_unused_media_count' ) );
     42
     43        // Handle AJAX: remove all unused media
     44        add_action( 'wp_ajax_remove_all_unused_media', array( $this, 'handle_remove_all_unused_media' ) );
     45
     46        // Hook into option updates to clear cache when site icon or theme mods change
     47        add_action( 'updated_option', array( $this, 'handle_option_update' ), 10, 3 );
     48    }
     49
     50    /**
     51     * Clear unused media cache when site icon or theme settings change.
     52     *
     53     * @param string $option    Option name.
     54     * @param mixed  $old_value Old option value.
     55     * @param mixed  $new_value New option value.
     56     */
     57    public function handle_option_update( $option, $old_value, $new_value ) {
     58        $ids_to_remove = [];
     59
     60        if ( 'site_icon' === $option && ! empty( $new_value ) ) {
     61            $ids_to_remove[] = intval( $new_value );
     62        } elseif ( 0 === strpos( $option, 'theme_mods_' ) ) {
     63            // Theme mods updated
     64            $mods = $new_value;
     65            if ( is_array( $mods ) ) {
     66                if ( ! empty( $mods['custom_logo'] ) ) {
     67                    $ids_to_remove[] = intval( $mods['custom_logo'] );
     68                }
     69                if ( ! empty( $mods['background_image_thumb'] ) ) {
     70                    $ids_to_remove[] = intval( $mods['background_image_thumb'] );
     71                }
     72                if ( ! empty( $mods['header_image_data'] ) && is_array( $mods['header_image_data'] ) && isset( $mods['header_image_data']['attachment_id'] ) ) {
     73                    $ids_to_remove[] = intval( $mods['header_image_data']['attachment_id'] );
     74                }
     75            }
     76        }
     77
     78        if ( ! empty( $ids_to_remove ) ) {
     79            // Retrieve snapshot
     80            $snapshot = get_option( 'media_tracker_unused_ids_snapshot', array() );
     81            if ( ! empty( $snapshot ) ) {
     82                $original_count = count( $snapshot );
     83                $snapshot = array_diff( $snapshot, $ids_to_remove );
     84
     85                if ( count( $snapshot ) !== $original_count ) {
     86                    // Update snapshot if changed
     87                    update_option( 'media_tracker_unused_ids_snapshot', $snapshot, false );
     88                }
     89            }
     90
     91            // Also clear cache to be safe
     92            $list = new Unused_Media_List( '', null );
     93            if ( method_exists( $list, 'force_clear_cache' ) ) {
     94                $list->force_clear_cache();
     95            }
     96        }
    4597    }
    4698
     
    48100     * Add media page to WordPress admin menu.
    49101     */
    50     public function register_unused_media_cleaner_menu() {
     102    public function register_media_tracker_menu() {
    51103        add_media_page(
    52             __( 'Unused Media', 'media-tracker' ),
    53             __( 'Unused Media', 'media-tracker' ),
     104            __( 'Media Tracker', 'media-tracker' ),
     105            __( 'Media Tracker', 'media-tracker' ),
    54106            'manage_options',
    55             'unused-media-cleaner',
    56             array( $this, 'uic_admin_page' )
     107            'media-tracker',
     108            array( $this, 'media_tracker_admin_page' )
    57109        );
    58110    }
    59111
    60     /**
    61      * Callback function to render the unused media admin page content.
    62      */
    63     public function uic_admin_page() {
    64         // Suppress other plugin admin notices on this specific screen
    65         if ( function_exists( 'remove_all_actions' ) ) {
    66             remove_all_actions( 'admin_notices' );
    67             remove_all_actions( 'all_admin_notices' );
    68             remove_all_actions( 'user_admin_notices' );
    69         }
    70 
    71         // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    72         $search    = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
    73         // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    74         $author_id = isset( $_GET['author'] ) ? intval( $_GET['author'] ) : null;
    75 
    76         // Validate author_id if provided
    77         if ( null !== $author_id && $author_id > 0 ) {
    78             $author_user = get_userdata( $author_id );
    79             if ( false === $author_user ) {
    80                 $author_id = null;
    81             }
    82         } else {
    83             $author_id = null;
    84         }
    85 
    86         // Create instance of Unused_Media_List and prepare items
    87         $unused_media_list = new Unused_Media_List( $search, $author_id );
    88         $unused_media_list->prepare_items();
    89 
    90         // Include the view template for displaying the list
    91         include __DIR__ . '/views/unused-media-list.php';
    92     }
    93 
    94     /**
    95      * Set the screen option value when it's updated.
    96      */
    97     public function set_screen_option( $status, $option, $value ) {
    98         if ( 'unused_media_cleaner_per_page' === $option ) {
    99             return (int) $value;
    100         }
    101         return $status;
    102     }
    103 
    104     /**
    105      * Add screen options for media page.
    106      */
    107     public function add_screen_options() {
    108         add_screen_option( 'per_page', array( 'label' => __( 'Unused Media per page', 'media-tracker' ), 'default' => 10, 'option' => 'unused_media_cleaner_per_page' ) );
    109     }
    110 
    111     /**
    112      * Suppress admin notices on the Unused Media screen to avoid other plugins' ads.
    113      */
    114     public function suppress_admin_notices() {
    115         if ( function_exists( 'remove_all_actions' ) ) {
    116             remove_all_actions( 'admin_notices' );
    117             remove_all_actions( 'all_admin_notices' );
    118             remove_all_actions( 'user_admin_notices' );
    119         }
     112    public function media_tracker_admin_page() {
     113        include __DIR__ . '/views/media-tracker.php';
    120114    }
    121115
     
    141135    public function handle_get_scan_progress() {
    142136        if ( ! current_user_can( 'manage_options' ) ) {
    143             wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 );
    144         }
    145 
    146         check_ajax_referer( 'media_tracker_nonce', 'nonce' );
     137            wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 );
     138        }
     139
     140        // Check nonce manually
     141        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) {
     142            wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 );
     143        }
    147144
    148145        $progress_key = 'media_scan_progress_' . get_current_user_id();
     
    177174     */
    178175    public function handle_run_media_scan() {
    179         if ( ! current_user_can( 'manage_options' ) ) {
    180             wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 );
    181         }
    182 
    183         check_ajax_referer( 'media_tracker_nonce', 'nonce' );
     176        // Check user capabilities
     177        if ( ! current_user_can( 'manage_options' ) ) {
     178            wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 );
     179        }
     180
     181        // Check nonce manually to return proper JSON error
     182        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) {
     183            wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'media-tracker' ) ), 403 );
     184        }
    184185
    185186        $user_id = get_current_user_id();
     
    188189        $progress_key = 'media_scan_progress_' . $user_id;
    189190        set_transient( $progress_key, array(
    190             'step' => 1,
     191            'step' => 0,
    191192            'total_steps' => 6,
    192193            'current_step' => 'Starting scan...',
     
    214215    public function handle_run_media_scan_sync() {
    215216        if ( ! current_user_can( 'manage_options' ) ) {
    216             wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 );
    217         }
    218 
    219         check_ajax_referer( 'media_tracker_nonce', 'nonce' );
     217            wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 );
     218        }
     219
     220        // Check nonce manually
     221        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) {
     222            wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'media-tracker' ) ), 403 );
     223        }
    220224
    221225        $user_id = get_current_user_id();
     
    224228        // Initialize progress to visible state
    225229        $progress = array(
    226             'step' => 1,
     230            'step' => 0,
    227231            'total_steps' => 6,
    228232            'current_step' => 'Starting scan...',
     
    240244            $list->force_clear_cache();
    241245        }
    242         $list->prepare_items();
     246
     247        // Use new method to scan and save snapshot
     248        if ( method_exists( $list, 'scan_and_save_snapshot' ) ) {
     249            $list->scan_and_save_snapshot();
     250        } else {
     251            // Fallback for safety
     252            $list->prepare_items();
     253        }
    243254
    244255        // Mark complete
     
    282293        $progress = get_transient( $progress_key );
    283294        if ( ! is_array( $progress ) ) {
    284             $progress = array( 'step' => 1, 'total_steps' => 6, 'current_step' => 'Starting scan...', 'used_ids' => array() );
     295            $progress = array( 'step' => 0, 'total_steps' => 6, 'current_step' => 'Starting scan...', 'used_ids' => array() );
    285296        }
    286297        $progress['current_step'] = 'Scanning...';
     
    290301            $list->force_clear_cache();
    291302        }
    292         $list->prepare_items();
     303
     304        // Use new method to scan and save snapshot
     305        if ( method_exists( $list, 'scan_and_save_snapshot' ) ) {
     306            $list->scan_and_save_snapshot();
     307        } else {
     308            // Fallback for safety
     309            $list->prepare_items();
     310        }
    293311
    294312        // Mark progress as complete
     
    305323    public function handle_clear_scan_progress() {
    306324        if ( ! current_user_can( 'manage_options' ) ) {
    307             wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 );
    308         }
    309 
    310         check_ajax_referer( 'media_tracker_nonce', 'nonce' );
     325            wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 );
     326        }
     327
     328        // Check nonce manually
     329        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) {
     330            wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 );
     331        }
    311332
    312333        $progress_key = 'media_scan_progress_' . get_current_user_id();
    313334        delete_transient( $progress_key );
    314335
     336        // Also clear the dashboard stats cache so the overview tab reflects the new scan results
     337        delete_transient( 'media_tracker_dashboard_stats_v6' );
     338
    315339        wp_send_json_success( array( 'message' => __( 'Progress cleared.', 'media-tracker' ) ) );
    316340    }
     
    321345    public function handle_get_unused_media_count() {
    322346        if ( ! current_user_can( 'manage_options' ) ) {
    323             wp_send_json_error( array( 'message' => __( 'Unauthorized', 'media-tracker' ) ), 403 );
    324         }
    325 
    326         check_ajax_referer( 'media_tracker_nonce', 'nonce' );
     347            wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 );
     348        }
     349
     350        // Check nonce manually
     351        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) {
     352            wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 );
     353        }
    327354
    328355        $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
     
    344371        ) );
    345372    }
     373
     374    /**
     375     * Handle AJAX request to remove all unused media.
     376     */
     377    public function handle_remove_all_unused_media() {
     378        if ( ! current_user_can( 'manage_options' ) ) {
     379            wp_send_json_error( array( 'message' => __( 'Unauthorized: You do not have permission to perform this action.', 'media-tracker' ) ), 403 );
     380        }
     381
     382        // Check nonce manually
     383        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'media_tracker_nonce' ) ) {
     384            wp_send_json_error( array( 'message' => __( 'Security check failed.', 'media-tracker' ) ), 403 );
     385        }
     386
     387        // Get all unused media IDs using the new public method
     388        $list = new Unused_Media_List( '', null );
     389        $unused_ids = $list->get_unused_media_ids( '' );
     390
     391        $deleted_count = 0;
     392        $failed_count = 0;
     393
     394        // Delete each unused media item
     395        foreach ( $unused_ids as $post_id ) {
     396            $result = wp_delete_attachment( $post_id, true ); // true for force delete (bypass trash)
     397            if ( $result ) {
     398                $deleted_count++;
     399            } else {
     400                $failed_count++;
     401            }
     402        }
     403
     404        // Clear the cache after deletion
     405        if ( method_exists( $list, 'force_clear_cache' ) ) {
     406            $list->force_clear_cache();
     407        }
     408
     409        if ( $deleted_count > 0 ) {
     410            wp_send_json_success( array(
     411                'message' => sprintf(
     412                    /* translators: %d: number of items deleted */
     413                    _n( 'Successfully deleted %d unused media item.', 'Successfully deleted %d unused media items.', $deleted_count, 'media-tracker' ),
     414                    $deleted_count
     415                ),
     416                'deleted_count' => $deleted_count,
     417                'failed_count' => $failed_count
     418            ) );
     419        } else {
     420            wp_send_json_error( array(
     421                'message' => __( 'No unused media items found or failed to delete.', 'media-tracker' )
     422            ) );
     423        }
     424    }
    346425}
  • media-tracker/trunk/includes/Admin/Unused_Media_List.php

    r3432010 r3454648  
    66
    77use WP_List_Table;
    8 
    9 // Define constants for safe scanning
    10 if ( ! defined( 'MEDIA_TRACKER_MAX_ITERATIONS' ) ) {
    11     define( 'MEDIA_TRACKER_MAX_ITERATIONS', 10000 ); // Maximum while loop iterations
    12 }
    13 
    14 if ( ! defined( 'MEDIA_TRACKER_BATCH_SIZE' ) ) {
    15     define( 'MEDIA_TRACKER_BATCH_SIZE', 100 ); // Batch size for processing
    16 }
    17 
    18 if ( ! defined( 'MEDIA_TRACKER_MAX_ATTACHMENT_ID' ) ) {
    19     define( 'MEDIA_TRACKER_MAX_ATTACHMENT_ID', 999999999 ); // Maximum valid attachment ID
    20 }
    218
    229/**
     
    119106                $author_url  = add_query_arg(
    120107                    [
    121                         'page'   => 'unused-media-cleaner',
     108                        'page'   => 'media-tracker',
    122109                        'author' => $item->post_author,
    123110                    ],
     
    135122                return date_i18n( 'Y/m/d', strtotime( $item->post_date ) );
    136123            default:
    137                 return '';
     124                return isset( $item->$column_name ) ? esc_html( $item->$column_name ) : '';
    138125        }
    139126    }
     
    179166        }
    180167
    181         // Safely increase memory and time limits
    182         if ( function_exists( 'ini_set' ) ) {
    183             // Increase memory limit with fallback to current limit
    184             $current_memory = ini_get( 'memory_limit' );
    185             if ( $current_memory && $current_memory !== '-1' ) {
    186                 // Convert to bytes for comparison
    187                 $current_bytes = wp_convert_hr_to_bytes( $current_memory );
    188                 $desired_bytes = wp_convert_hr_to_bytes( '512M' );
    189 
    190                 if ( $desired_bytes > $current_bytes ) {
    191                     // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged
    192                     @ini_set( 'memory_limit', '512M' );
    193                 }
    194             } else {
    195                 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged
    196                 @ini_set( 'memory_limit', '512M' );
    197             }
    198 
    199             // Increase execution time safely
    200             if ( ! stristr( php_sapi_name(), 'cli' ) ) {
    201                 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged
    202                 @set_time_limit( 300 );
    203             }
     168        wp_raise_memory_limit( 'admin' );
     169        if ( function_exists( 'set_time_limit' ) ) {
     170            // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Scanning can take a long time.
     171            set_time_limit( 300 );
    204172        }
    205173
     
    209177            set_transient( $progress_key, $progress, 300 );
    210178
    211             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    212             $featured_images = $wpdb->get_col("
     179            $featured_images = $this->get_cached_db_result("
    213180                SELECT DISTINCT meta_value
    214181                FROM {$wpdb->postmeta}
     
    218185            ");
    219186            if ( $featured_images ) {
    220                 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {
    221                     $progress['used_ids'] = array();
    222                 }
    223187                $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $featured_images ) );
    224188            }
     
    231195            set_transient( $progress_key, $progress, 300 );
    232196
    233             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    234             $gallery_images = $wpdb->get_col("
     197            $gallery_images = $this->get_cached_db_result("
    235198                SELECT DISTINCT meta_value
    236199                FROM {$wpdb->postmeta}
     
    242205            foreach ( $gallery_images as $gallery_string ) {
    243206                if ( ! empty( $gallery_string ) ) {
    244                     if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {
    245                         $progress['used_ids'] = array();
    246                     }
    247207                    $gallery_ids = explode( ',', $gallery_string );
    248208                    $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', array_filter( $gallery_ids ) ) );
     
    250210            }
    251211
    252             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    253             $variation_images = $wpdb->get_col("
     212            $variation_images = $this->get_cached_db_result("
    254213                SELECT DISTINCT meta_value
    255214                FROM {$wpdb->postmeta}
     
    263222            ");
    264223            if ( $variation_images ) {
    265                 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {
    266                     $progress['used_ids'] = array();
    267                 }
    268224                $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $variation_images ) );
    269225            }
     
    271227        }
    272228
    273         // Step 2.5: WooCommerce Taxonomy Images (Categories, Tags, etc.)
     229        $used_image_ids = $progress['used_ids'];
     230
     231        // Ensure featured images are always included
     232        $featured_images = $this->get_cached_db_result("
     233            SELECT DISTINCT meta_value
     234            FROM {$wpdb->postmeta}
     235            WHERE meta_key = '_thumbnail_id'
     236            AND meta_value != '0'
     237            AND meta_value != ''
     238        ");
     239        if ( $featured_images ) {
     240            $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $featured_images ) );
     241        }
     242
    274243        if ( class_exists( 'WooCommerce' ) ) {
    275             // Get WooCommerce product category images
    276             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    277             $category_images = $wpdb->get_col("
    278                 SELECT tm.meta_value
    279                 FROM {$wpdb->termmeta} tm
    280                 INNER JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id
    281                 WHERE tt.taxonomy IN ('product_cat', 'product_tag')
    282                 AND tm.meta_key = 'thumbnail_id'
    283                 AND tm.meta_value != ''
    284                 AND tm.meta_value != '0'
     244            $gallery_images = $this->get_cached_db_result("
     245                SELECT DISTINCT meta_value
     246                FROM {$wpdb->postmeta}
     247                WHERE meta_key = '_product_image_gallery'
     248                AND meta_value != ''
     249                AND meta_value IS NOT NULL
    285250            ");
    286251
    287             if ( $category_images ) {
    288                 if ( ! isset( $progress['used_ids'] ) || ! is_array( $progress['used_ids'] ) ) {
    289                     $progress['used_ids'] = array();
    290                 }
    291                 $progress['used_ids'] = array_merge( $progress['used_ids'], array_map( 'intval', $category_images ) );
    292             }
    293         }
    294 
    295         // Ensure used_ids is set and is an array
    296         $used_image_ids = isset( $progress['used_ids'] ) && is_array( $progress['used_ids'] )
    297             ? $progress['used_ids']
    298             : array();
    299 
    300         // Check for ACF using both class and function for better compatibility
    301         if ( class_exists( 'ACF' ) || function_exists( 'acf' ) ) {
    302             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    303             $acf_meta_values = $wpdb->get_col("
     252            foreach ( $gallery_images as $gallery_string ) {
     253                if ( ! empty( $gallery_string ) ) {
     254                    $gallery_ids = explode( ',', $gallery_string );
     255                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', array_filter( $gallery_ids ) ) );
     256                }
     257            }
     258
     259            $variation_images = $this->get_cached_db_result("
     260                SELECT DISTINCT meta_value
     261                FROM {$wpdb->postmeta}
     262                WHERE meta_key = '_thumbnail_id'
     263                AND post_id IN (
     264                    SELECT ID FROM {$wpdb->posts}
     265                    WHERE post_type = 'product_variation'
     266                )
     267                AND meta_value != '0'
     268                AND meta_value != ''
     269            ");
     270            if ( $variation_images ) {
     271                $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $variation_images ) );
     272            }
     273        }
     274
     275        if ( class_exists( 'ACF' ) ) {
     276            $acf_meta_values = $this->get_cached_db_result("
    304277                SELECT DISTINCT pm.meta_value
    305278                FROM {$wpdb->postmeta} pm
     
    326299
    327300                if ( is_serialized( $meta_value ) ) {
    328                     $unserialized = unserialize( $meta_value );
     301                    $unserialized = @unserialize( $meta_value );
    329302                    if ( is_array( $unserialized ) ) {
    330303                        array_walk_recursive( $unserialized, function( $item ) use ( &$used_image_ids ) {
    331                             if ( is_numeric( $item ) && $item > 0 && $item < MEDIA_TRACKER_MAX_ATTACHMENT_ID ) {
     304                            if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) {
    332305                                $used_image_ids[] = intval( $item );
    333306                            }
     
    341314                    if ( is_array( $json_decoded ) ) {
    342315                        array_walk_recursive( $json_decoded, function( $item ) use ( &$used_image_ids ) {
    343                             if ( is_numeric( $item ) && $item > 0 && $item < MEDIA_TRACKER_MAX_ATTACHMENT_ID ) {
     316                            if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) {
    344317                                $used_image_ids[] = intval( $item );
    345318                            }
     
    376349            }
    377350
    378             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    379             $acf_specific_query = $wpdb->get_col("
     351            $acf_specific_query = $this->get_cached_db_result("
    380352                SELECT DISTINCT pm.meta_value
    381353                FROM {$wpdb->postmeta} pm
     
    402374                } else {
    403375                    if ( is_serialized( $acf_value ) ) {
    404                         $unserialized = unserialize( $acf_value );
     376                        $unserialized = @unserialize( $acf_value );
    405377                        if ( is_array( $unserialized ) ) {
    406378                            array_walk_recursive( $unserialized, function( $item ) use ( &$used_image_ids ) {
    407                                 if ( is_numeric( $item ) && $item > 0 && $item < MEDIA_TRACKER_MAX_ATTACHMENT_ID ) {
     379                                if ( is_numeric( $item ) && $item > 0 && $item < 999999999 ) {
    408380                                    $used_image_ids[] = intval( $item );
    409381                                }
     
    415387        }
    416388
    417         $batch_size = MEDIA_TRACKER_BATCH_SIZE;
     389        $batch_size = 100;
    418390        $offset = 0;
    419         $iterations = 0;
    420 
    421         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
     391
     392        while ( true ) {
    422393            $sql = $wpdb->prepare(
    423394                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     
    426397                $offset
    427398            );
    428             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    429             $posts_with_content = $wpdb->get_results( $sql );
     399            $posts_with_content = $this->get_cached_db_result( $sql, 'results' );
    430400
    431401            if ( empty( $posts_with_content ) ) {
     
    436406                preg_match_all( '/wp-image-(\d+)/', $post->post_content, $matches );
    437407                if ( ! empty( $matches[1] ) ) {
    438                     if ( ! is_array( $used_image_ids ) ) {
    439                         $used_image_ids = array();
    440                     }
    441408                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
    442409                }
     
    444411
    445412            $offset += $batch_size;
    446             $iterations++;
    447 
    448             // Safety check: if we got fewer results than batch_size, we're done
    449             if ( count( $posts_with_content ) < $batch_size ) {
    450                 break;
    451             }
    452413        }
    453414
    454415        // Scan Gutenberg image blocks that may not include the wp-image- class
    455416        $offset_blocks = 0;
    456         $iterations = 0;
    457 
    458         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
     417        while ( true ) {
    459418            $sql_blocks = $wpdb->prepare(
    460419                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     
    463422                $offset_blocks
    464423            );
    465             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    466             $posts_with_blocks = $wpdb->get_results( $sql_blocks );
     424            $posts_with_blocks = $this->get_cached_db_result( $sql_blocks, 'results' );
    467425
    468426            if ( empty( $posts_with_blocks ) ) {
     
    477435
    478436            $offset_blocks += $batch_size;
    479             $iterations++;
    480 
    481             if ( count( $posts_with_blocks ) < $batch_size ) {
    482                 break;
    483             }
    484         }
    485 
    486         // Scan Gutenberg cover blocks with background images
    487         $offset_cover = 0;
    488         $iterations = 0;
    489 
    490         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
    491             $sql_cover = $wpdb->prepare(
    492                 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     437        }
     438
     439        // Scan other Gutenberg media blocks (Cover, Media & Text, Video, Audio, File)
     440        $offset_blocks_ext = 0;
     441        while ( true ) {
     442            $sql_blocks_ext = $wpdb->prepare(
     443                "SELECT ID, post_content FROM {$wpdb->posts} WHERE (
     444                    post_content LIKE %s OR
     445                    post_content LIKE %s OR
     446                    post_content LIKE %s OR
     447                    post_content LIKE %s OR
     448                    post_content LIKE %s
     449                ) AND post_status = 'publish' LIMIT %d OFFSET %d",
    493450                '%wp:cover%',
    494                 $batch_size,
    495                 $offset_cover
    496             );
    497             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    498             $posts_with_cover = $wpdb->get_results( $sql_cover );
    499 
    500             if ( empty( $posts_with_cover ) ) {
    501                 break;
    502             }
    503 
    504             foreach ( $posts_with_cover as $post ) {
    505                 // Match: <!-- wp:cover {"url":"...","id":123} -->
    506                 if ( preg_match_all( '/<!--\s*wp:cover\s*{[^}]*"id"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    507                     $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
    508                 }
    509             }
    510 
    511             $offset_cover += $batch_size;
    512             $iterations++;
    513 
    514             if ( count( $posts_with_cover ) < $batch_size ) {
    515                 break;
    516             }
    517         }
    518 
    519         // Scan Gutenberg media-text blocks
    520         $offset_mediatext = 0;
    521         $iterations = 0;
    522 
    523         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
    524             $sql_mediatext = $wpdb->prepare(
    525                 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
    526451                '%wp:media-text%',
    527                 $batch_size,
    528                 $offset_mediatext
    529             );
    530             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    531             $posts_with_mediatext = $wpdb->get_results( $sql_mediatext );
    532 
    533             if ( empty( $posts_with_mediatext ) ) {
    534                 break;
    535             }
    536 
    537             foreach ( $posts_with_mediatext as $post ) {
    538                 // Match: <!-- wp:media-text {"mediaId":123} -->
    539                 if ( preg_match_all( '/<!--\s*wp:media-text\s*{[^}]*"mediaId"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    540                     $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
    541                 }
    542             }
    543 
    544             $offset_mediatext += $batch_size;
    545             $iterations++;
    546 
    547             if ( count( $posts_with_mediatext ) < $batch_size ) {
    548                 break;
    549             }
    550         }
    551 
    552         // Scan Gutenberg file blocks
    553         $offset_file = 0;
    554         $iterations = 0;
    555 
    556         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
    557             $sql_file = $wpdb->prepare(
    558                 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     452                '%wp:video%',
     453                '%wp:audio%',
    559454                '%wp:file%',
    560455                $batch_size,
    561                 $offset_file
    562             );
    563             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    564             $posts_with_file = $wpdb->get_results( $sql_file );
    565 
    566             if ( empty( $posts_with_file ) ) {
     456                $offset_blocks_ext
     457            );
     458            $posts_with_blocks_ext = $this->get_cached_db_result( $sql_blocks_ext, 'results' );
     459
     460            if ( empty( $posts_with_blocks_ext ) ) {
    567461                break;
    568462            }
    569463
    570             foreach ( $posts_with_file as $post ) {
    571                 // Match: <!-- wp:file {"id":123} -->
    572                 if ( preg_match_all( '/<!--\s*wp:file\s*{[^}]*"id"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
     464            foreach ( $posts_with_blocks_ext as $post ) {
     465                // Cover block
     466                if ( preg_match_all( '/<!--\s*wp:cover\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    573467                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
    574468                }
    575             }
    576 
    577             $offset_file += $batch_size;
    578             $iterations++;
    579 
    580             if ( count( $posts_with_file ) < $batch_size ) {
    581                 break;
    582             }
    583         }
    584 
    585         // Scan Gutenberg video and audio blocks
    586         $offset_media = 0;
    587         $iterations = 0;
    588 
    589         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
    590             $sql_media = $wpdb->prepare(
    591                 "SELECT ID, post_content FROM {$wpdb->posts} WHERE (post_content LIKE %s OR post_content LIKE %s) AND post_status = 'publish' LIMIT %d OFFSET %d",
    592                 '%wp:video%',
    593                 '%wp:audio%',
    594                 $batch_size,
    595                 $offset_media
    596             );
    597             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    598             $posts_with_media = $wpdb->get_results( $sql_media );
    599 
    600             if ( empty( $posts_with_media ) ) {
    601                 break;
    602             }
    603 
    604             foreach ( $posts_with_media as $post ) {
    605                 // Match video/audio poster images: <!-- wp:video {"poster":123,"id":456} -->
    606                 if ( preg_match_all( '/<!--\s*wp:(video|audio)\s*{[^}]*"id"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    607                     $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[2] ) );
    608                 }
    609                 // Match poster images separately
    610                 if ( preg_match_all( '/<!--\s*wp:(video|audio)\s*{[^}]*"poster"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    611                     $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[2] ) );
    612                 }
    613             }
    614 
    615             $offset_media += $batch_size;
    616             $iterations++;
    617 
    618             if ( count( $posts_with_media ) < $batch_size ) {
    619                 break;
    620             }
    621         }
    622 
    623         // Scan old caption shortcodes with attachment IDs
    624         $offset_caption = 0;
    625         $iterations = 0;
    626 
    627         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
    628             $sql_caption = $wpdb->prepare(
    629                 "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
    630                 '%[caption%',
    631                 $batch_size,
    632                 $offset_caption
    633             );
    634             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    635             $posts_with_caption = $wpdb->get_results( $sql_caption );
    636 
    637             if ( empty( $posts_with_caption ) ) {
    638                 break;
    639             }
    640 
    641             foreach ( $posts_with_caption as $post ) {
    642                 // Match: [caption id="attachment_123" ...] or [caption id="123" ...]
    643                 if ( preg_match_all( '/\[caption[^\]]*id\s*=\s*["\']?attachment_(\d+)["\']?/', $post->post_content, $matches ) ) {
     469                // Media & Text block (uses mediaId)
     470                if ( preg_match_all( '/<!--\s*wp:media-text\s*{[^}]*\"mediaId\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    644471                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
    645472                }
    646                 // Also match caption with numeric id without attachment_ prefix
    647                 if ( preg_match_all( '/\[caption[^\]]*id\s*=\s*["\']?(\d+)["\']?(?:[^\]]*\s*class\s*=[^\]]*wp-caption)/', $post->post_content, $matches ) ) {
     473                // Video block
     474                if ( preg_match_all( '/<!--\s*wp:video\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
    648475                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
    649476                }
    650             }
    651 
    652             $offset_caption += $batch_size;
    653             $iterations++;
    654 
    655             if ( count( $posts_with_caption ) < $batch_size ) {
    656                 break;
    657             }
     477                // Audio block
     478                if ( preg_match_all( '/<!--\s*wp:audio\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
     479                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
     480                }
     481                // File block
     482                if ( preg_match_all( '/<!--\s*wp:file\s*{[^}]*\"id\"\s*:\s*(\d+)/', $post->post_content, $matches ) ) {
     483                    $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
     484                }
     485            }
     486
     487            $offset_blocks_ext += $batch_size;
    658488        }
    659489
    660490        // Scan gallery shortcodes and Gutenberg gallery blocks
    661491        $offset_gallery = 0;
    662         $iterations = 0;
    663 
    664         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
     492        while ( true ) {
    665493            $sql_gallery = $wpdb->prepare(
    666494                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status = 'publish' LIMIT %d OFFSET %d",
     
    669497                $offset_gallery
    670498            );
    671             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    672             $posts_with_gallery = $wpdb->get_results( $sql_gallery );
     499            $posts_with_gallery = $this->get_cached_db_result( $sql_gallery, 'results' );
    673500
    674501            if ( empty( $posts_with_gallery ) ) {
     
    696523
    697524            $offset_gallery += $batch_size;
    698             $iterations++;
    699 
    700             if ( count( $posts_with_gallery ) < $batch_size ) {
    701                 break;
    702             }
    703525        }
    704526
    705527        // Elementor: scan all posts with _elementor_data in batches and extract image IDs more accurately
    706528        $el_offset = 0;
    707         $iterations = 0;
    708 
    709         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
     529        while ( true ) {
    710530            $sql_el = $wpdb->prepare(
    711531                "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_elementor_data' LIMIT %d OFFSET %d",
     
    713533                $el_offset
    714534            );
    715             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    716             $elementor_posts = $wpdb->get_col( $sql_el );
     535            $elementor_posts = $this->get_cached_db_result( $sql_el );
    717536
    718537            if ( empty( $elementor_posts ) ) {
     
    740559                            }
    741560                        }
     561
     562                        // Check for 'ids' key (arrays or comma-separated strings), common in galleries
     563                        if ( isset( $node['ids'] ) ) {
     564                            $ids_val = $node['ids'];
     565                            if ( is_array( $ids_val ) ) {
     566                                foreach ( $ids_val as $id ) {
     567                                    if ( is_numeric( $id ) && $id > 0 ) {
     568                                        $used_image_ids[] = intval( $id );
     569                                    }
     570                                }
     571                            } elseif ( is_string( $ids_val ) ) {
     572                                $ids = array_filter( array_map( 'intval', explode( ',', $ids_val ) ) );
     573                                if ( ! empty( $ids ) ) {
     574                                    $used_image_ids = array_merge( $used_image_ids, $ids );
     575                                }
     576                            }
     577                        }
     578
    742579                        foreach ( $node as $v ) {
    743580                            $extract_ids( $v );
     
    750587
    751588            $el_offset += $batch_size;
    752             $iterations++;
    753 
    754             if ( count( $elementor_posts ) < $batch_size ) {
    755                 break;
    756             }
    757589        }
    758590
    759591        // Divi Builder: scan posts using Divi shortcodes and extract image IDs/URLs
    760592        $divi_offset = 0;
    761         $iterations = 0;
    762 
    763         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
     593        while ( true ) {
    764594            $sql_divi = $wpdb->prepare(
    765595                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d",
     
    768598                $divi_offset
    769599            );
    770             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    771             $divi_posts = $wpdb->get_results( $sql_divi );
     600            $divi_posts = $this->get_cached_db_result( $sql_divi, 'results' );
    772601
    773602            if ( empty( $divi_posts ) ) {
     
    810639
    811640            $divi_offset += $batch_size;
    812             $iterations++;
    813 
    814             if ( count( $divi_posts ) < $batch_size ) {
    815                 break;
    816             }
    817641        }
    818642
    819643        // Generic: scan direct uploads URLs in post content and map to attachment IDs
    820644        $offset_urls = 0;
    821         $iterations = 0;
    822 
    823         while ( $iterations < MEDIA_TRACKER_MAX_ITERATIONS ) {
     645        while ( true ) {
    824646            $sql_urls = $wpdb->prepare(
    825647                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT %d OFFSET %d",
     
    828650                $offset_urls
    829651            );
    830             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    831             $posts_with_urls = $wpdb->get_results( $sql_urls );
     652            $posts_with_urls = $this->get_cached_db_result( $sql_urls, 'results' );
    832653
    833654            if ( empty( $posts_with_urls ) ) {
     
    848669
    849670            $offset_urls += $batch_size;
    850             $iterations++;
    851 
    852             if ( count( $posts_with_urls ) < $batch_size ) {
     671        }
     672
     673        // Scan post_excerpt (e.g. WooCommerce Short Description)
     674        $offset_excerpt = 0;
     675        while ( true ) {
     676            $sql_excerpt = $wpdb->prepare(
     677                "SELECT ID, post_excerpt FROM {$wpdb->posts} WHERE post_status = 'publish' AND (post_excerpt LIKE %s OR post_excerpt LIKE %s) LIMIT %d OFFSET %d",
     678                '%wp-content/uploads%',
     679                '%wp-image-%',
     680                $batch_size,
     681                $offset_excerpt
     682            );
     683            $posts_with_excerpt = $this->get_cached_db_result( $sql_excerpt, 'results' );
     684
     685            if ( empty( $posts_with_excerpt ) ) {
    853686                break;
    854687            }
     688
     689            foreach ( $posts_with_excerpt as $post ) {
     690                // Check for wp-image- ID
     691                if ( preg_match_all( '/wp-image-(\d+)/', $post->post_excerpt, $matches ) ) {
     692                    if ( ! empty( $matches[1] ) ) {
     693                        $used_image_ids = array_merge( $used_image_ids, array_map( 'intval', $matches[1] ) );
     694                    }
     695                }
     696
     697                // Check for direct URLs
     698                if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $post->post_excerpt, $m_url_all ) ) {
     699                    foreach ( $m_url_all[0] as $url ) {
     700                        $id = attachment_url_to_postid( $url );
     701                        if ( $id ) {
     702                            $used_image_ids[] = intval( $id );
     703                        }
     704                    }
     705                }
     706            }
     707
     708            $offset_excerpt += $batch_size;
     709        }
     710
     711        // Scan postmeta for raw URLs (custom fields, metaboxes)
     712        $offset_meta = 0;
     713        while ( true ) {
     714            $sql_meta = $wpdb->prepare(
     715                "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_value LIKE %s LIMIT %d OFFSET %d",
     716                '%wp-content/uploads%',
     717                $batch_size,
     718                $offset_meta
     719            );
     720            $meta_values = $this->get_cached_db_result( $sql_meta, 'col' );
     721
     722            if ( empty( $meta_values ) ) {
     723                break;
     724            }
     725
     726            foreach ( $meta_values as $val ) {
     727                // Check for direct URLs
     728                if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all ) ) {
     729                    foreach ( $m_url_all[0] as $url ) {
     730                        // Clean up URL (remove query strings, etc if needed, though attachment_url_to_postid handles some)
     731                        $id = attachment_url_to_postid( $url );
     732                        if ( $id ) {
     733                            $used_image_ids[] = intval( $id );
     734                        }
     735                    }
     736                }
     737            }
     738            $offset_meta += $batch_size;
     739        }
     740
     741        // Scan options for raw URLs (theme settings, custom options)
     742        $offset_options = 0;
     743        while ( true ) {
     744            $sql_options = $wpdb->prepare(
     745                "SELECT option_value FROM {$wpdb->options} WHERE option_value LIKE %s LIMIT %d OFFSET %d",
     746                '%wp-content/uploads%',
     747                $batch_size,
     748                $offset_options
     749            );
     750            $option_values = $this->get_cached_db_result( $sql_options, 'col' );
     751
     752            if ( empty( $option_values ) ) {
     753                break;
     754            }
     755
     756            foreach ( $option_values as $val ) {
     757                // Check for direct URLs
     758                if ( preg_match_all( "/https?:\/\/[^\"']+\/wp-content\/uploads\/[^\"']+/i", $val, $m_url_all ) ) {
     759                    foreach ( $m_url_all[0] as $url ) {
     760                        $id = attachment_url_to_postid( $url );
     761                        if ( $id ) {
     762                            $used_image_ids[] = intval( $id );
     763                        }
     764                    }
     765                }
     766            }
     767            $offset_options += $batch_size;
    855768        }
    856769
     
    876789
    877790        $used_image_ids = array_unique( array_filter( array_map( 'intval', $used_image_ids ), function( $id ) {
    878             return $id > 0 && $id < MEDIA_TRACKER_MAX_ATTACHMENT_ID;
     791            return $id > 0 && $id < 999999999;
    879792        }));
    880793
    881794        return $used_image_ids;
     795    }
     796
     797    public function scan_and_save_snapshot() {
     798        global $wpdb;
     799
     800        // Force calculation of used IDs
     801        $used_image_ids = $this->get_used_media_ids();
     802
     803        // Get all attachment IDs
     804        $all_attachments = $this->get_cached_db_result( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit'" );
     805
     806        // Calculate unused IDs
     807        $unused_ids = array_diff( $all_attachments, $used_image_ids );
     808
     809        // Filter and sanitize
     810        $unused_ids = array_unique( array_filter( array_map( 'intval', $unused_ids ) ) );
     811
     812        // Calculate size
     813        $total_size = 0;
     814        foreach ( $unused_ids as $id ) {
     815            $file_path = get_attached_file( $id );
     816            if ( $file_path && file_exists( $file_path ) ) {
     817                $total_size += filesize( $file_path );
     818            }
     819        }
     820
     821        // Save snapshot and stats
     822        update_option( 'media_tracker_unused_ids_snapshot', $unused_ids, false );
     823        update_option( 'unused_media_last_cache_time', time() );
     824        update_option( 'media_tracker_unused_count_last_scan', count( $unused_ids ) );
     825        update_option( 'media_tracker_unused_size_last_scan', $total_size );
     826
     827        // Invalidate dashboard stats cache to ensure overview tab is updated
     828        delete_transient( 'media_tracker_dashboard_stats_v8' );
     829
     830        return count($unused_ids);
    882831    }
    883832
     
    887836        $this->display_delete_message();
    888837
    889         // Verify nonce for search and filter actions
    890         if ( isset( $_REQUEST['s'] ) || isset( $_GET['refresh_cache'] ) ) {
    891             check_admin_referer( 'unused-media-filter' );
    892         }
    893 
    894         $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
     838        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Search request is a GET request and safe.
     839        $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
    895840
    896841        $per_page     = $this->get_items_per_page( 'unused_media_cleaner_per_page', 10 );
     
    898843        $offset       = ( $current_page - 1 ) * $per_page;
    899844
    900         $force_refresh = isset( $_GET['refresh_cache'] ) && $_GET['refresh_cache'] === '1';
    901 
    902         $cache_key = 'unused_media_list_v2_' . md5( serialize( array( $search, $this->author_id, $current_page, $per_page ) ) );
    903 
    904         if ( $force_refresh || $this->should_invalidate_cache() ) {
    905             delete_transient( $cache_key );
    906         }
    907 
    908         $cached_results = get_transient( $cache_key );
    909 
    910         if ( false === $cached_results || $force_refresh ) {
    911             $used_image_ids = $this->get_used_media_ids();
    912 
    913             $where_conditions = [
    914                 "p.post_type = 'attachment'",
    915                 "p.post_status = 'inherit'"
    916             ];
    917 
    918             if ( ! empty( $used_image_ids ) ) {
    919                 // Ensure all IDs are integers and sanitize
    920                 $sanitized_ids = array_map( 'intval', $used_image_ids );
    921                 $sanitized_ids = array_filter( $sanitized_ids, function( $id ) {
    922                     return $id > 0;
    923                 } );
    924 
    925                 if ( ! empty( $sanitized_ids ) ) {
    926                     // Escape IDs properly to avoid SQL injection
    927                     $escaped_ids = implode( ',', array_map( 'absint', $sanitized_ids ) );
    928                     $where_conditions[] = "p.ID NOT IN ($escaped_ids)";
    929                 }
    930             }
     845        // Retrieve from snapshot
     846        $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() );
     847
     848        // Handle search if needed (filter snapshot IDs by search term)
     849        // This requires a query if search is present, but restricted to snapshot IDs.
     850
     851        $where_conditions = [
     852            "p.post_type = 'attachment'",
     853            "p.post_status = 'inherit'"
     854        ];
     855
     856        if ( empty( $unused_image_ids ) ) {
     857            // No unused media found in snapshot (or not scanned yet)
     858            $this->items = array();
     859            $total_items = 0;
     860        } else {
     861            $args = array(
     862                'post_type'      => 'attachment',
     863                'post_status'    => 'inherit',
     864                'post__in'       => $unused_image_ids,
     865                'posts_per_page' => $per_page,
     866                'paged'          => $current_page,
     867                'orderby'        => $orderby_param,
     868                'order'          => $order_param,
     869            );
    931870
    932871            if ( $this->author_id ) {
    933                 $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id );
     872                $args['author'] = $this->author_id;
    934873            }
    935874
    936875            if ( $search ) {
    937                 $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
    938             }
    939 
    940             $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions );
    941 
    942             // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    943             $count_query = "
    944                 SELECT COUNT(*)
    945                 FROM {$wpdb->posts} p
    946                 $where_clause
    947             ";
    948             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    949             $total_items = $wpdb->get_var( $count_query );
    950 
    951             // Construct the base query with WHERE clause - phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    952             // The $where_clause is safely constructed from sanitized/prepared values
    953             $base_query = "SELECT p.ID, p.post_title, p.guid, p.post_author, p.post_date
    954                 FROM {$wpdb->posts} p
    955                 $where_clause
    956                 ORDER BY p.post_date DESC";
    957 
    958             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    959             $this->items = $wpdb->get_results( $wpdb->prepare( $base_query . ' LIMIT %d, %d', $offset, $per_page ) );
    960 
    961             set_transient( $cache_key, array( 'items' => $this->items, 'total_items' => $total_items ), 1800 );
    962 
    963             update_option( 'unused_media_last_cache_time', time() );
    964         } else {
    965             $this->items = $cached_results['items'];
    966             $total_items = $cached_results['total_items'];
     876                $args['s'] = $search;
     877            }
     878
     879            $query = new \WP_Query( $args );
     880            $this->items = $query->posts;
     881            $total_items = $query->found_posts;
    967882        }
    968883
     
    975890            'total_items' => $total_items,
    976891            'per_page'    => $per_page,
    977             'total_pages' => ceil( $total_items / $per_page ),
    978          ) );
     892            'total_pages' => $per_page > 0 ? ceil( $total_items / $per_page ) : 0,
     893        ) );
    979894    }
    980895
     
    991906        $last_cache_time = get_option( 'unused_media_last_cache_time', 0 );
    992907
    993         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    994         $recent_media_activity = $wpdb->get_var( $wpdb->prepare(
    995             "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_modified_gmt > %s",
     908        $recent_media_activity = $this->get_cached_db_result( $wpdb->prepare(
     909            "SELECT COUNT(*)\n            FROM {$wpdb->posts}\n            WHERE post_type = 'attachment'\n            AND post_modified_gmt > %s",
    996910            gmdate( 'Y-m-d H:i:s', $last_cache_time )
    997         ) );
    998 
    999         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    1000         $recent_post_activity = $wpdb->get_var( $wpdb->prepare(
    1001             "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type IN ('post', 'page', 'product') AND post_modified_gmt > %s",
     911        ), 'var' );
     912
     913        $recent_post_activity = $this->get_cached_db_result( $wpdb->prepare(
     914            "SELECT COUNT(*)\n            FROM {$wpdb->posts}\n            WHERE post_type IN ('post', 'page', 'product')\n            AND post_modified_gmt > %s",
    1002915            gmdate( 'Y-m-d H:i:s', $last_cache_time )
    1003         ) );
     916        ), 'var' );
    1004917
    1005918        return ( $recent_media_activity > 0 || $recent_post_activity > 0 );
     
    1014927        global $wpdb;
    1015928
    1016         // Verify nonce for cache refresh actions
    1017         if ( isset( $_GET['refresh_cache'] ) ) {
    1018             check_admin_referer( 'unused-media-filter' );
    1019         }
    1020 
    1021         $force_refresh = ( isset( $_GET['refresh_cache'] ) && $_GET['refresh_cache'] === '1' ) || $force_fresh;
    1022 
    1023         $cache_key = 'unused_media_total_' . md5( serialize( array( $search, $this->author_id ) ) );
    1024 
    1025         if ( $force_refresh || $this->should_invalidate_cache() ) {
    1026             delete_transient( $cache_key );
    1027         }
    1028 
    1029         $cached_total = get_transient( $cache_key );
    1030 
    1031         if ( false !== $cached_total && ! $force_refresh ) {
    1032             return $cached_total;
    1033         }
    1034 
    1035         $used_image_ids = $this->get_used_media_ids();
     929        // Retrieve from snapshot
     930        $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() );
     931
     932        if ( empty( $unused_image_ids ) ) {
     933            return 0;
     934        }
    1036935
    1037936        $where_conditions = [
     
    1040939        ];
    1041940
    1042         if ( ! empty( $used_image_ids ) ) {
    1043             // Ensure all IDs are integers and sanitize
    1044             $sanitized_ids = array_map( 'intval', $used_image_ids );
    1045             $sanitized_ids = array_filter( $sanitized_ids, function( $id ) {
    1046                 return $id > 0;
    1047             } );
    1048 
    1049             if ( ! empty( $sanitized_ids ) ) {
    1050                 // Escape IDs properly to avoid SQL injection
    1051                 $escaped_ids = implode( ',', array_map( 'absint', $sanitized_ids ) );
    1052                 $where_conditions[] = "p.ID NOT IN ($escaped_ids)";
    1053             }
    1054         }
     941        $ids_placeholder = implode( ',', array_map( 'intval', $unused_image_ids ) );
     942        $where_conditions[] = "p.ID IN ($ids_placeholder)";
    1055943
    1056944        if ( $this->author_id ) {
     
    1064952        $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions );
    1065953
    1066         // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    1067954        $query = "
    1068955            SELECT COUNT(*)
     
    1070957            $where_clause
    1071958        ";
    1072         // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    1073         $total_items = $wpdb->get_var( $query );
    1074 
    1075         set_transient( $cache_key, $total_items, 1800 );
    1076 
    1077         update_option( 'unused_media_last_cache_time', time() );
    1078 
    1079         return $total_items;
     959
     960        return $this->get_cached_db_result( $query, 'var' );
    1080961    }
    1081962
    1082963    public function get_fresh_total_items( $search = '' ) {
    1083964        return $this->get_total_items( $search, true );
     965    }
     966
     967    /**
     968     * Get all unused media IDs.
     969     *
     970     * @param string $search Optional search term.
     971     * @return array Array of unused media IDs.
     972     */
     973    public function get_unused_media_ids( $search = '' ) {
     974        global $wpdb;
     975
     976        // Retrieve from snapshot
     977        $unused_image_ids = get_option( 'media_tracker_unused_ids_snapshot', array() );
     978
     979        if ( empty( $unused_image_ids ) ) {
     980            return array();
     981        }
     982
     983        // Build query to get all unused media
     984        $where_conditions = [
     985            "p.post_type = 'attachment'",
     986            "p.post_status = 'inherit'"
     987        ];
     988
     989        $ids_placeholder = implode( ',', array_map( 'intval', $unused_image_ids ) );
     990        $where_conditions[] = "p.ID IN ($ids_placeholder)";
     991
     992        if ( $this->author_id ) {
     993            $where_conditions[] = $wpdb->prepare( 'p.post_author = %d', $this->author_id );
     994        }
     995
     996        if ( $search ) {
     997            $where_conditions[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $search ) . '%' );
     998        }
     999
     1000        $where_clause = 'WHERE ' . implode( ' AND ', $where_conditions );
     1001
     1002        $query = "
     1003            SELECT p.ID
     1004            FROM {$wpdb->posts} p
     1005            $where_clause
     1006        ";
     1007
     1008        return $this->get_cached_db_result( $query );
    10841009    }
    10851010
     
    11251050        global $wpdb;
    11261051
    1127         // Use WordPress cache API for better compatibility
    1128         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    1129         $transients = $wpdb->get_col( $wpdb->prepare(
    1130             "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
    1131             '_transient_unused_media_%'
    1132         ) );
    1133 
    1134         foreach ( $transients as $transient ) {
    1135             $key = str_replace( '_transient_', '', $transient );
    1136             delete_transient( $key );
    1137         }
     1052        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     1053        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_unused_media_%'" );
     1054        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     1055        $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_unused_media_%'" );
    11381056
    11391057        delete_option( 'unused_media_last_cache_time' );
     1058    }
     1059
     1060    /**
     1061     * Helper to execute DB queries with caching.
     1062     *
     1063     * @param string $query The SQL query.
     1064     * @param string $type  The type of query result ('col', 'var', 'results').
     1065     * @return mixed Query result.
     1066     */
     1067    private function get_cached_db_result( $query, $type = 'col' ) {
     1068        global $wpdb;
     1069
     1070        $key = 'mt_db_' . md5( $query );
     1071        $group = 'media_tracker';
     1072        $result = wp_cache_get( $key, $group );
     1073
     1074        if ( false === $result ) {
     1075            if ( 'col' === $type ) {
     1076                $result = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
     1077            } elseif ( 'results' === $type ) {
     1078                $result = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
     1079            } elseif ( 'var' === $type ) {
     1080                $result = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
     1081            }
     1082            wp_cache_set( $key, $result, $group, 300 );
     1083        }
     1084        return $result;
    11401085    }
    11411086
  • media-tracker/trunk/includes/Admin/views/unused-media-list.php

    r3432010 r3454648  
    1111    wp_localize_script( 'mt-admin-script', 'mediaTracker', array(
    1212        'nonce' => wp_create_nonce( 'media_tracker_nonce' ),
    13         'ajax_url' => admin_url( 'admin-ajax.php' ),
     13        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    1414    ) );
    1515}
    16 
    1716?>
    1817
    19 <div class="wrap unused-media-list">
    20     <h1 class="wp-heading-inline">
    21         <svg fill="#fff" width="40px" height="40px" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" stroke="#fff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M46.4375 0.03125C45.539063 0.0390625 44.695313 0.398438 44.21875 1.125L36.625 15.40625C37.1875 15.601563 38.453125 16.164063 42.65625 18.0625L42.71875 18.09375C43.445313 18.421875 44 18.65625 44.21875 18.75C44.292969 18.785156 44.363281 18.839844 44.4375 18.875L49.96875 3.5625C50.316406 2.351563 49.449219 0.957031 48.0625 0.40625C47.546875 0.148438 46.976563 0.0273438 46.4375 0.03125 Z M 4 8C1.792969 8 0 9.792969 0 12C0 14.207031 1.792969 16 4 16C6.207031 16 8 14.207031 8 12C8 9.792969 6.207031 8 4 8 Z M 13 11C11.894531 11 11 11.894531 11 13C11 14.105469 11.894531 15 13 15C14.105469 15 15 14.105469 15 13C15 11.894531 14.105469 11 13 11 Z M 32.15625 16.625C30.222656 16.769531 28.539063 17.730469 27.34375 19.40625C28.097656 20.675781 29.417969 22.226563 31.28125 22.1875C31.773438 22.167969 32.1875 22.523438 32.28125 23C32.660156 23.589844 34.988281 24.636719 35.65625 24.375C35.9375 24.265625 36.238281 24.289063 36.5 24.4375C36.761719 24.585938 36.949219 24.828125 37 25.125C37.039063 25.289063 37.476563 25.863281 38.375 26.28125C39.082031 26.609375 39.769531 26.691406 40.15625 26.5C40.40625 26.375 40.679688 26.371094 40.9375 26.46875C41.199219 26.566406 41.425781 26.773438 41.53125 27.03125C42.207031 28.679688 45.292969 28.800781 47.40625 28.625C47.714844 27.285156 47.632813 25.890625 47.15625 24.59375C46.496094 22.808594 45.1875 21.398438 43.40625 20.59375C43.21875 20.511719 42.613281 20.222656 41.84375 19.875C38.28125 18.265625 36.269531 17.390625 35.875 17.28125C34.570313 16.765625 33.316406 16.539063 32.15625 16.625 Z M 11.5 18C8.46875 18 6 20.46875 6 23.5C6 26.53125 8.46875 29 11.5 29C14.53125 29 17 26.53125 17 23.5C17 20.46875 14.53125 18 11.5 18 Z M 26.28125 21.40625C25.96875 22.148438 25.613281 22.84375 25.25 23.5C25.679688 24.546875 26.949219 26.972656 29.28125 26.4375C29.550781 26.375 29.835938 26.410156 30.0625 26.5625C30.292969 26.714844 30.421875 26.949219 30.46875 27.21875C30.535156 27.59375 30.976563 28.039063 31.59375 28.375C32.46875 28.847656 33.414063 28.953125 33.8125 28.78125C34.074219 28.667969 34.367188 28.660156 34.625 28.78125C34.882813 28.902344 35.078125 29.132813 35.15625 29.40625C35.296875 29.882813 35.789063 30.371094 36.46875 30.71875C37.269531 31.125 38.183594 31.273438 38.78125 31.0625C39.242188 30.902344 39.734375 31.097656 39.96875 31.53125C40.851563 33.167969 43.75 33.34375 46 33.1875C46.285156 32.375 46.550781 31.539063 46.8125 30.65625C46.542969 30.671875 46.261719 30.6875 45.96875 30.6875C43.875 30.6875 41.371094 30.273438 40.125 28.5625C39.28125 28.675781 38.3125 28.492188 37.34375 28C36.640625 27.640625 35.867188 27.089844 35.40625 26.40625C34.132813 26.40625 32.667969 25.699219 31.9375 25.25C31.371094 24.902344 30.929688 24.558594 30.65625 24.1875C28.671875 24.003906 27.253906 22.710938 26.28125 21.40625 Z M 24 25.46875C17.800781 34.082031 7.214844 33.828125 7.09375 33.8125C6.699219 33.777344 6.3125 33.988281 6.125 34.34375C5.9375 34.699219 5.964844 35.125 6.21875 35.4375C8.003906 37.640625 9.921875 39.503906 11.875 41.09375C12.796875 41.277344 18.597656 42.097656 24.34375 35.4375C24.703125 35.019531 25.332031 34.984375 25.75 35.34375C26.167969 35.703125 26.203125 36.332031 25.84375 36.75C21.835938 41.394531 17.609375 42.847656 14.65625 43.15625C17.125 44.820313 19.613281 46.078125 21.9375 47.03125C23.414063 46.722656 28.367188 45.242188 32.75 38.5625C33.054688 38.101563 33.695313 37.945313 34.15625 38.25C34.617188 38.554688 34.742188 39.195313 34.4375 39.65625C31.132813 44.691406 27.515625 47.054688 24.96875 48.15625C30.167969 49.839844 34.046875 49.988281 34.375 50L34.40625 50C34.59375 50 34.777344 49.945313 34.9375 49.84375C35.21875 49.667969 41.007813 45.886719 45.25 35.25C45.085938 35.253906 44.917969 35.28125 44.75 35.28125C42.5625 35.28125 40.035156 34.839844 38.65625 33.125C37.6875 33.242188 36.578125 33.019531 35.5625 32.5C34.734375 32.074219 34.078125 31.503906 33.65625 30.84375C32.59375 30.933594 31.445313 30.550781 30.65625 30.125C29.84375 29.683594 29.207031 29.128906 28.84375 28.5C26.542969 28.621094 24.945313 27.054688 24 25.46875Z"></path></g></svg>
    22         <?php echo esc_html__( 'Unused Media Files', 'media-tracker' ); ?>
    23     </h1>
    24 
    25     <div class="media-toolbar-wrap wp-filter">
     18<div class="unused-media-list">
     19    <div class="media-header">
     20        <div class="section-title">
     21            <h2><i class="dashicons dashicons-format-image"></i> <?php esc_html_e( 'Unused Media', 'media-tracker' ); ?></h2>
     22            <p class="page-subtitle">
     23                <?php esc_html_e( 'Detect media files not used anywhere on your site for safe cleanup.', 'media-tracker' ); ?>
     24            </p>
     25        </div>
     26
    2627        <div class="unused-image-found">
    2728            <?php
    2829                // Use the same cached total as the table to ensure consistency.
    29                 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
    30                 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    31                 $mt_search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.Security.NonceVerification.Recommended
    32                 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
    33                 $mt_initial_count = $unused_media_list->get_total_items( $mt_search );
     30                $media_tracker_search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     31                $media_tracker_initial_count = $media_tracker_unused_media_list->get_total_items( $media_tracker_search );
    3432            ?>
    35             <h2><?php echo '<span id="unused-count">'.esc_html( $mt_initial_count ).'</span>' . ' ' . esc_html__( 'Unused Media Found!', 'media-tracker' ); ?></h2>
    36         </div>
    37 
    38         <div class="search-form">
    39             <div class="media-scan-controls" style="margin: 16px 0;">
    40                 <button id="run-media-scan" class="button button-primary">
    41                     <?php
    42                         // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
    43                         $last_scan_time = (int) get_option( 'unused_media_last_cache_time', 0 );
    44                         echo esc_html( $last_scan_time ? __( 'Rescan', 'media-tracker' ) : __( 'Run Scan in Background', 'media-tracker' ) );
    45                     ?>
    46                 </button>
    47             </div>
    48 
    49             <?php $unused_media_list->search_box( esc_html__( 'Search Media', 'media-tracker' ), 'media-search' ); ?>
     33            <h2><?php echo '<span id="unused-count">'.esc_html( $media_tracker_initial_count ).'</span>' . ' ' . esc_html__( 'unused media found!', 'media-tracker' ); ?></h2>
    5034        </div>
    5135    </div>
    5236
    53     <div id="media-scan-progress" style="display: none; margin: 16px 0; padding: 12px; border: 1px solid #ccd0d4; background: #fff;">
     37    <div id="media-scan-progress" style="display: none; margin: 16px 0 20px; padding: 14px 15px 18px; border: 1px solid #e0e0e0; border-radius: 3px;">
    5438        <p id="media-scan-progress-text" style="margin: 0 0 8px;">
    5539            <?php esc_html_e( 'Scan status: Ready to scan...', 'media-tracker' ); ?>
    5640        </p>
    57         <div id="media-scan-progress-bar" style="position: relative; height: 16px; background: #f0f0f0; border: 1px solid #ccd0d4;">
    58             <div id="media-scan-progress-fill" style="height: 100%; width: 0; background: #46b450; transition: width 1000ms ease; will-change: width;"></div>
     41        <div id="media-scan-progress-bar" style="position: relative; height: 15px; background: #f0f0f0; border: 1px solid #ccd0d4; border-radius: 4px; overflow: hidden; ">
     42            <div id="media-scan-progress-fill" style="height: 100%; width: 0; background: #6366f1; background-size: 200% 100%; transition: width 0.3s ease-in-out; will-change: width;"></div>
     43            <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent); background-size: 20px 20px; "></div>
    5944        </div>
    6045    </div>
    6146
     47    <!-- Fallback container for scan button when no items exist -->
     48    <div class="media-scan-controls" style="display: none; margin: 20px 0; padding: 15px; background: #fff; border: 1px solid #c3c4c7; border-left: 4px solid #6366f1; box-shadow: 0 1px 1px rgba(0,0,0,.04);"></div>
     49
    6250    <form method="post">
    6351        <?php
    64             $unused_media_list->display();
     52            $media_tracker_unused_media_list->display();
    6553        ?>
    6654    </form>
     
    6957<script type="text/javascript">
    7058(function($){
    71     // Ensure mediaTracker and ajax_url are defined before using
     59    // Ensure mediaTracker and ajaxUrl are defined before using
    7260    if (typeof window.mediaTracker === 'undefined') {
    7361        window.mediaTracker = {
     
    7664        };
    7765    }
    78     var AJAX_URL = (window.mediaTracker && window.mediaTracker.ajax_url) || (typeof ajaxurl !== 'undefined' ? ajaxurl : '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>');
     66    var AJAX_URL = (window.mediaTracker && (window.mediaTracker.ajax_url || window.mediaTracker.ajaxUrl)) || (typeof ajaxurl !== 'undefined' ? ajaxurl : '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>');
    7967
    8068    var NONCE = (window.mediaTracker && window.mediaTracker.nonce) || '<?php echo esc_js( wp_create_nonce( 'media_tracker_nonce' ) ); ?>';
     
    8270    var stuckChecks = 0; // consecutive polls stuck near start
    8371    var syncTriggered = false;
     72    var clientProgress = 0; // Client-side progress simulation
     73    var targetProgress = 0; // Target progress from server
     74    var progressAnimInterval = null; // Animation interval
    8475
    8576    function ensureScanButtonExists(label){
     
    10091            $('.media-scan-controls').hide();
    10192        } else {
    102             $('.media-scan-controls').show().empty().append($newBtn);
    103         }
     93            // No bulkactions, use fallback container with message
     94            var $message = $('<p/>', {
     95                html: '<strong>No unused media found.</strong> Click the button below to scan your site for unused media files.',
     96                css: { margin: '0 0 12px 0', color: '#646970' }
     97            });
     98            $('.media-scan-controls').show().empty().append($message).append($newBtn);
     99        }
     100
     101        // Add Remove All button if it doesn't exist
     102        if ($('#remove-all-unused-media').length === 0) {
     103            var $removeBtn = $('<button/>', { id: 'remove-all-unused-media', class: 'button button-secondary', text: 'Remove all unused media' });
     104            $removeBtn.css({ marginLeft: '8px' });
     105            $newBtn.after($removeBtn);
     106        }
     107
    104108        return $newBtn;
    105109    }
     
    119123            $('.media-scan-controls').hide();
    120124        } else {
    121             $('.media-scan-controls').show();
     125            // No bulkactions found (no items), use fallback container
     126            $('.media-scan-controls').show().empty().append($btn);
    122127        }
    123128    }
     
    140145    }
    141146
     147    // Smooth progress animation function
     148    function animateProgress(){
     149        // Gradually move clientProgress towards targetProgress
     150        if (clientProgress < targetProgress) {
     151            // Increase gradually based on distance
     152            var diff = targetProgress - clientProgress;
     153            var increment = diff > 20 ? 3 : (diff > 10 ? 2 : 1); // Faster when far, slower when close
     154
     155            clientProgress += increment;
     156            if (clientProgress > targetProgress) {
     157                clientProgress = targetProgress;
     158            }
     159            $('#media-scan-progress-fill').css('width', clientProgress + '%');
     160        }
     161    }
     162
    142163    function updateProgress(){
    143164        $.post(AJAX_URL, { action: 'get_media_scan_progress', nonce: NONCE }).done(function(res){
    144             if (!res || !res.success || !res.data) return;
     165            if (!res || !res.success || !res.data) {
     166                // If no data yet, simulate gradual progress
     167                if (targetProgress < 90) {
     168                    targetProgress += 5; // Add 5% gradually
     169                }
     170                return;
     171            }
     172
    145173            var d = res.data;
    146174            var pct = Math.max(0, Math.min(100, parseInt(d.percentage, 10) || 0));
    147             $('#media-scan-progress-fill').css('width', pct + '%');
     175            targetProgress = pct; // Update target from server
     176
    148177            $('#media-scan-progress-text').text('Scan status: ' + (d.current_step || '') + ' (' + pct + '%)');
    149178
     
    152181                if ((d.step <= 1) && (pct <= 20)) {
    153182                    stuckChecks++;
    154                     if (stuckChecks >= 3) { // ~9s at 3s interval
     183                    if (stuckChecks >= 6) { // Changed from 3 to 6 (now ~3s at 500ms interval)
    155184                        syncTriggered = true;
    156185                        $.post(AJAX_URL, { action: 'run_media_scan_sync', nonce: NONCE }).always(function(){
     
    164193
    165194            if (pct >= 100 || d.step >= d.total_steps) {
     195                // Stop animations
    166196                clearInterval(pollInterval);
     197                clearInterval(progressAnimInterval);
     198                clientProgress = 100;
     199                targetProgress = 100;
     200                $('#media-scan-progress-fill').css('width', '100%');
     201
    167202                // Clear progress transient to avoid sticky progress UI
    168203                $.post(AJAX_URL, {
     
    174209                        action: 'get_unused_media_count',
    175210                        nonce: NONCE,
    176                         search: '<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?><?php echo esc_js( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' ); ?>',
    177                         author_id: '<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?><?php echo esc_js( isset( $_GET['author'] ) ? intval( $_GET['author'] ) : '' ); ?>'
     211                        search: '<?php echo esc_js( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>',
     212                        author_id: '<?php echo esc_js( isset( $_GET['author'] ) ? intval( $_GET['author'] ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>'
    178213                    }).done(function(countRes){
    179214                        if (countRes && countRes.success && countRes.data) {
    180                             $('#media-scan-progress-text').text('Scan complete (100%) - ' + countRes.data.message);
     215                            $('#media-scan-progress-text').html('✅ <strong>Scan Complete!</strong> - ' + countRes.data.message);
    181216                            $('#unused-count').text(countRes.data.count);
    182217                        } else {
    183                             $('#media-scan-progress-text').text('Scan complete (100%)');
     218                            $('#media-scan-progress-text').html('✅ <strong>Scan Complete!</strong> (100%)');
    184219                        }
    185220                    }).fail(function(){
    186                         $('#media-scan-progress-text').text('Scan complete (100%)');
     221                        $('#media-scan-progress-text').html('✅ <strong>Scan Complete!</strong> (100%)');
    187222                    }).always(function(){
    188223                        ensureScanButtonExists('Rescan').prop('disabled', false);
    189224                        // refresh list table so item count and pagination match new cache
    190225                        refreshListTable('Rescan');
    191                         // keep progress bar visible for 15 seconds, then hide
     226                        // keep progress bar visible for 3 seconds, then reload page
    192227                        setTimeout(function(){
    193                             $('#media-scan-progress').hide();
    194                         }, 15000);
     228                            $('#media-scan-progress').fadeOut(300, function(){
     229                                // Reload page to show fresh data
     230                                location.reload();
     231                            });
     232                        }, 3000); // Reduced from 15000 to 3000
    195233                    });
    196234                });
     
    201239    function startPolling(){
    202240        if (pollInterval) { clearInterval(pollInterval); }
    203         pollInterval = setInterval(updateProgress, 1000);
     241        if (progressAnimInterval) { clearInterval(progressAnimInterval); }
     242
     243        // Start smooth animation interval (runs every 50ms)
     244        progressAnimInterval = setInterval(animateProgress, 50);
     245
     246        // Start server polling (every 500ms)
     247        pollInterval = setInterval(updateProgress, 500);
    204248        updateProgress();
    205249    }
     
    207251    $(document).on('click', '#run-media-scan', function(e){
    208252        e.preventDefault();
     253        e.stopPropagation();
     254
    209255        var $btn = $(this);
    210         $('#media-scan-progress').show();
     256
     257        // Prevent double clicks
     258        if ($btn.prop('disabled')) {
     259            return false;
     260        }
     261
     262        // Reset progress variables
     263        clientProgress = 0;
     264        targetProgress = 5; // Start with 5%
     265        syncTriggered = false;
     266        stuckChecks = 0;
     267
     268        // Show progress bar with animation
     269        var $progress = $('#media-scan-progress');
     270        $progress.css('display', 'block').hide().fadeIn(300);
     271
    211272        $('#media-scan-progress-fill').css('width', '0%');
    212273        $('#media-scan-progress-text').text('Scan status: Starting... (0%)');
     
    218279        }).done(function(res){
    219280            $btn.text('Scanning...');
     281
     282            // Start smooth animation immediately
     283            clientProgress = 0;
     284            targetProgress = 10; // Jump to 10% quickly
     285            animateProgress();
     286
    220287            startPolling();
    221         }).fail(function(){
    222             $('#media-scan-progress').hide();
     288        }).fail(function(xhr, status, error){
     289            $('#media-scan-progress-text').html('❌ <strong>Error:</strong> ' + (xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Failed to start scan. Please refresh and try again.'));
     290            setTimeout(function(){
     291                $('#media-scan-progress').fadeOut(300);
     292            }, 3000);
    223293            ensureScanButtonExists('Scan Unused Media').prop('disabled', false);
    224294        });
     295
     296        return false;
     297    });
     298
     299    // Handle Remove All Unused Media button click
     300    $(document).on('click', '#remove-all-unused-media', function(e){
     301        e.preventDefault();
     302        e.stopPropagation();
     303
     304        var $btn = $(this);
     305
     306        // Prevent double clicks
     307        if ($btn.prop('disabled')) {
     308            return false;
     309        }
     310
     311        // Show confirmation alert
     312        if (!confirm('Are you sure you want to delete all unused media? This action cannot be undone. Continue?')) {
     313            return false;
     314        }
     315
     316        $btn.prop('disabled', true).text('Deleting...');
     317
     318        // Show progress bar
     319        var $progress = $('#media-scan-progress');
     320        $progress.css('display', 'block').hide().fadeIn(300);
     321        $('#media-scan-progress-text').text('Deleting all unused media...');
     322        $('#media-scan-progress-fill').css('width', '0%');
     323
     324        $.post(AJAX_URL, {
     325            action: 'remove_all_unused_media',
     326            nonce: NONCE
     327        }).done(function(res){
     328            if (res && res.success) {
     329                $('#media-scan-progress-text').html('✅ <strong>Success!</strong> - ' + (res.data.message || 'All unused media has been deleted.'));
     330                $('#media-scan-progress-fill').css('width', '100%');
     331                $('#unused-count').text('0');
     332
     333                // Refresh the list after 2 seconds
     334                setTimeout(function(){
     335                    location.reload();
     336                }, 2000);
     337            } else {
     338                throw new Error(res.data && res.data.message || 'Unknown error');
     339            }
     340        }).fail(function(xhr, status, error){
     341            var errorMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Failed to delete unused media.';
     342            $('#media-scan-progress-text').html('❌ <strong>Error:</strong> ' + errorMsg);
     343            setTimeout(function(){
     344                $('#media-scan-progress').fadeOut(300);
     345            }, 3000);
     346            $btn.prop('disabled', false).text('Remove all unused media');
     347        });
     348
     349        return false;
    225350    });
    226351
  • media-tracker/trunk/includes/Installer.php

    r3441211 r3454648  
    2121        $this->add_version();
    2222        $this->optimize_database_indexes();
     23        $this->schedule_cron_jobs();
     24    }
     25
     26    /**
     27     * Schedule cron jobs for background processing
     28     *
     29     * @since   1.2.3
     30     * @access  public
     31     * @param   none
     32     * @return  void
     33     */
     34    public function schedule_cron_jobs() {
     35        // Schedule the batch processing cron job
     36        if (!wp_next_scheduled('media_tracker_batch_process')) {
     37            // Prefer the faster schedule when available
     38            $interval = 'five_minutes';
     39            $schedules = wp_get_schedules();
     40            if (!isset($schedules[$interval])) {
     41                $interval = 'hourly';
     42            }
     43            wp_schedule_event(time(), $interval, 'media_tracker_batch_process');
     44        }
     45    }
     46
     47    /**
     48     * Clear scheduled cron jobs on deactivation
     49     *
     50     * @since   1.2.3
     51     * @access  public
     52     * @param   none
     53     * @return  void
     54     */
     55    public static function clear_cron_jobs() {
     56        // Clear the batch processing cron job
     57        $timestamp = wp_next_scheduled('media_tracker_batch_process');
     58        if ($timestamp) {
     59            wp_unschedule_event($timestamp, 'media_tracker_batch_process');
     60        }
     61
     62        // Also clear any scheduled hooks that might have been set
     63        wp_clear_scheduled_hook('media_tracker_batch_process');
    2364    }
    2465
  • media-tracker/trunk/languages/media-tracker.pot

    r3432010 r3454648  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Media Tracker 1.2.1\n"
     5"Project-Id-Version: Media Tracker 1.3.0\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-01-04T08:21:49+00:00\n"
     12"POT-Creation-Date: 2026-02-05T12:57: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
     20#: includes/Admin/Menu.php:105
    1921msgid "Media Tracker"
    2022msgstr ""
     
    3537msgstr ""
    3638
    37 #: includes/Admin/Duplicate_Images.php:32
    38 msgid "Every 5 Minutes"
    39 msgstr ""
    40 
    41 #: includes/Admin/Duplicate_Images.php:64
     39#: includes/Admin/Duplicate_Images.php:43
    4240msgid "Duplicates"
    4341msgstr ""
    4442
    45 #: includes/Admin/Duplicate_Images.php:66
     43#: includes/Admin/Duplicate_Images.php:45
    4644msgid "Duplicate images filter"
    4745msgstr ""
    4846
    49 #: includes/Admin/Duplicate_Images.php:67
     47#: includes/Admin/Duplicate_Images.php:46
    5048msgid "All Media"
    5149msgstr ""
    5250
    53 #: includes/Admin/Duplicate_Images.php:69
     51#: includes/Admin/Duplicate_Images.php:48
    5452msgid "Show Duplicate Images"
    5553msgstr ""
    5654
    57 #: includes/Admin/Duplicate_Images.php:74
     55#: includes/Admin/Duplicate_Images.php:53
    5856msgid "Re-scan"
    5957msgstr ""
    6058
    61 #: includes/Admin/Duplicate_Images.php:380
    62 #: includes/Admin/Duplicate_Images.php:589
    63 #: includes/Admin/Menu.php:124
    64 #: includes/Admin/Menu.php:143
    65 #: includes/Admin/Menu.php:180
    66 #: includes/Admin/Menu.php:216
    67 #: includes/Admin/Menu.php:307
    68 #: includes/Admin/Menu.php:323
     59#: includes/Admin/Duplicate_Images.php:376
     60#: includes/Admin/Duplicate_Images.php:564
     61#: includes/Admin/Duplicate_Images.php:611
     62#: includes/Admin/Menu.php:118
    6963msgid "Unauthorized"
    7064msgstr ""
    7165
    7266#. translators: 1: number of hashes reset, 2: number of images being rescanned
    73 #: includes/Admin/Duplicate_Images.php:616
     67#: includes/Admin/Duplicate_Images.php:600
    7468#, php-format
    7569msgid "Reset %1$d hashes. Re-scanning %2$d images..."
    7670msgstr ""
    7771
    78 #: includes/Admin/Media_Usage.php:36
     72#: includes/Admin/Duplicate_Images.php:617
     73msgid "No images selected."
     74msgstr ""
     75
     76#. translators: %d: number of deleted images
     77#: includes/Admin/Duplicate_Images.php:643
     78#, php-format
     79msgid "Deleted %d duplicate images."
     80msgstr ""
     81
     82#: includes/Admin/Duplicate_Images.php:649
     83msgid "No images were deleted."
     84msgstr ""
     85
     86#: includes/Admin/Duplicate_Images.php:684
     87msgid "items"
     88msgstr ""
     89
     90#: includes/Admin/Media_Usage.php:178
    7991msgid "Media Usage"
    8092msgstr ""
    8193
    82 #: includes/Admin/Media_Usage.php:51
     94#: includes/Admin/Media_Usage.php:193
     95#: includes/Admin/views/tabs/tab-duplicates.php:151
    8396msgid "Usages Count"
    8497msgstr ""
    8598
    86 #: includes/Admin/Media_Usage.php:73
     99#: includes/Admin/Media_Usage.php:215
    87100msgid "Open attachment edit screen"
    88101msgstr ""
    89102
    90 #: includes/Admin/Media_Usage.php:172
     103#: includes/Admin/Media_Usage.php:314
    91104msgid "#"
    92105msgstr ""
    93106
    94 #: includes/Admin/Media_Usage.php:173
     107#: includes/Admin/Media_Usage.php:315
     108#: includes/Admin/views/tabs/tab-duplicates.php:136
    95109msgid "Title"
    96110msgstr ""
    97111
    98 #: includes/Admin/Media_Usage.php:174
     112#: includes/Admin/Media_Usage.php:316
     113#: includes/Admin/views/tabs/tab-overview.php:304
     114#: includes/Admin/views/tabs/tab-overview.php:354
    99115msgid "Type"
    100116msgstr ""
    101117
    102 #: includes/Admin/Media_Usage.php:175
     118#: includes/Admin/Media_Usage.php:317
    103119msgid "Date Added"
    104120msgstr ""
    105121
    106 #: includes/Admin/Media_Usage.php:176
     122#: includes/Admin/Media_Usage.php:318
     123#: includes/Admin/views/tabs/tab-duplicates.php:158
     124#: includes/Admin/views/tabs/tab-overview.php:307
     125#: includes/Admin/views/tabs/tab-overview.php:356
    107126msgid "Actions"
    108127msgstr ""
    109128
    110 #: includes/Admin/Media_Usage.php:189
     129#: includes/Admin/Media_Usage.php:331
    111130msgid "System Setting"
    112131msgstr ""
    113132
    114 #: includes/Admin/Media_Usage.php:192
     133#: includes/Admin/Media_Usage.php:334
    115134msgid "Customize Site Icon"
    116135msgstr ""
    117136
    118137#. Translators: This is a time difference string
    119 #: includes/Admin/Media_Usage.php:202
     138#: includes/Admin/Media_Usage.php:344
    120139#, php-format
    121140msgid "%s ago"
    122141msgstr ""
    123142
    124 #: includes/Admin/Media_Usage.php:224
     143#: includes/Admin/Media_Usage.php:366
    125144msgid "Admin View"
    126145msgstr ""
    127146
    128 #: includes/Admin/Media_Usage.php:225
     147#: includes/Admin/Media_Usage.php:367
    129148msgid "Frontend View"
    130149msgstr ""
    131150
    132 #: includes/Admin/Media_Usage.php:233
     151#: includes/Admin/Media_Usage.php:375
    133152msgid "No posts or pages found using this media file."
    134153msgstr ""
    135154
    136 #: includes/Admin/Media_Usage.php:274
     155#: includes/Admin/Media_Usage.php:416
    137156msgid "Site Icon (Favicon)"
    138157msgstr ""
    139158
    140 #: includes/Admin/Menu.php:52
    141 #: includes/Admin/Menu.php:53
    142 msgid "Unused Media"
    143 msgstr ""
    144 
    145 #: includes/Admin/Menu.php:108
    146 msgid "Unused Media per page"
    147 msgstr ""
    148 
    149 #: includes/Admin/Menu.php:135
     159#: includes/Admin/Menu.php:129
    150160msgid "Cache cleared successfully."
    151161msgstr ""
    152162
    153 #: includes/Admin/Menu.php:207
     163#: includes/Admin/Menu.php:137
     164#: includes/Admin/Menu.php:178
     165#: includes/Admin/Menu.php:217
     166#: includes/Admin/Menu.php:325
     167#: includes/Admin/Menu.php:347
     168#: includes/Admin/Menu.php:379
     169msgid "Unauthorized: You do not have permission to perform this action."
     170msgstr ""
     171
     172#: includes/Admin/Menu.php:142
     173#: includes/Admin/Menu.php:330
     174#: includes/Admin/Menu.php:352
     175#: includes/Admin/Menu.php:384
     176msgid "Security check failed."
     177msgstr ""
     178
     179#: includes/Admin/Menu.php:183
     180#: includes/Admin/Menu.php:222
     181msgid "Security check failed. Please refresh the page and try again."
     182msgstr ""
     183
     184#: includes/Admin/Menu.php:208
    154185msgid "Scan started."
    155186msgstr ""
    156187
    157 #: includes/Admin/Menu.php:251
     188#: includes/Admin/Menu.php:262
    158189msgid "Scan completed."
    159190msgstr ""
    160191
    161 #: includes/Admin/Menu.php:315
     192#: includes/Admin/Menu.php:339
    162193msgid "Progress cleared."
    163194msgstr ""
    164195
    165196#. translators: %d: number of unused media found!
    166 #: includes/Admin/Menu.php:341
     197#: includes/Admin/Menu.php:368
    167198#, php-format
    168199msgid "%d unused image found"
     
    171202msgstr[1] ""
    172203
    173 #: includes/Admin/Unused_Media_List.php:67
     204#. translators: %d: number of items deleted
     205#: includes/Admin/Menu.php:413
     206#, php-format
     207msgid "Successfully deleted %d unused media item."
     208msgid_plural "Successfully deleted %d unused media items."
     209msgstr[0] ""
     210msgstr[1] ""
     211
     212#: includes/Admin/Menu.php:421
     213msgid "No unused media items found or failed to delete."
     214msgstr ""
     215
     216#: includes/Admin/PluginMeta.php:61
     217msgid "Settings"
     218msgstr ""
     219
     220#: includes/Admin/Unused_Media_List.php:54
    174221msgid "File"
    175222msgstr ""
    176223
    177 #: includes/Admin/Unused_Media_List.php:68
     224#: includes/Admin/Unused_Media_List.php:55
    178225msgid "Author"
    179226msgstr ""
    180227
    181 #: includes/Admin/Unused_Media_List.php:69
     228#: includes/Admin/Unused_Media_List.php:56
     229#: includes/Admin/views/tabs/tab-duplicates.php:137
     230#: includes/Admin/views/tabs/tab-overview.php:305
    182231msgid "Size"
    183232msgstr ""
    184233
    185 #: includes/Admin/Unused_Media_List.php:70
     234#: includes/Admin/Unused_Media_List.php:57
     235#: includes/Admin/views/tabs/tab-overview.php:306
    186236msgid "Date"
    187237msgstr ""
    188238
    189 #: includes/Admin/Unused_Media_List.php:95
     239#: includes/Admin/Unused_Media_List.php:82
    190240msgid "Edit"
    191241msgstr ""
    192242
    193 #: includes/Admin/Unused_Media_List.php:96
     243#: includes/Admin/Unused_Media_List.php:83
     244#: includes/Admin/views/tabs/tab-overview.php:329
     245#: includes/Admin/views/tabs/tab-overview.php:380
    194246msgid "View"
    195247msgstr ""
    196248
     249#: includes/Admin/Unused_Media_List.php:84
     250msgid "Delete Permanently"
     251msgstr ""
     252
     253#. translators: %s: post title
     254#: includes/Admin/Unused_Media_List.php:88
     255#, php-format
     256msgid "\"%s\" (Edit)"
     257msgstr ""
     258
    197259#: includes/Admin/Unused_Media_List.php:97
    198 msgid "Delete Permanently"
    199 msgstr ""
    200 
    201 #. translators: %s: post title
    202 #: includes/Admin/Unused_Media_List.php:101
    203 #, php-format
    204 msgid "\"%s\" (Edit)"
    205 msgstr ""
    206 
    207 #: includes/Admin/Unused_Media_List.php:110
    208260msgid "File name:"
    209261msgstr ""
    210262
    211 #: includes/Admin/Unused_Media_List.php:1094
     263#: includes/Admin/Unused_Media_List.php:1019
    212264msgid "Delete permanently"
    213265msgstr ""
    214266
    215267#. translators: %d: number of deleted media files
    216 #: includes/Admin/Unused_Media_List.php:1119
     268#: includes/Admin/Unused_Media_List.php:1044
    217269#, php-format
    218270msgid "%d media file(s) deleted successfully."
    219271msgstr ""
    220272
    221 #: includes/Admin/views/unused-media-list.php:22
    222 msgid "Unused Media Files"
    223 msgstr ""
    224 
    225 #: includes/Admin/views/unused-media-list.php:35
    226 msgid "Unused Media Found!"
    227 msgstr ""
    228 
    229 #: includes/Admin/views/unused-media-list.php:44
    230 msgid "Rescan"
    231 msgstr ""
    232 
    233 #: includes/Admin/views/unused-media-list.php:44
    234 msgid "Run Scan in Background"
    235 msgstr ""
    236 
    237 #: includes/Admin/views/unused-media-list.php:49
    238 msgid "Search Media"
    239 msgstr ""
    240 
    241 #: includes/Admin/views/unused-media-list.php:55
     273#: includes/Admin/views/media-tracker.php:7
     274msgid "MediaTracker"
     275msgstr ""
     276
     277#. translators: %s: Plugin version number.
     278#: includes/Admin/views/media-tracker.php:15
     279#, php-format
     280msgid "Version %s"
     281msgstr ""
     282
     283#: includes/Admin/views/media-tracker.php:61
     284msgid "Add New Connection"
     285msgstr ""
     286
     287#: includes/Admin/views/media-tracker.php:63
     288msgid "&times;"
     289msgstr ""
     290
     291#: includes/Admin/views/media-tracker.php:67
     292msgid "Connection Name"
     293msgstr ""
     294
     295#: includes/Admin/views/media-tracker.php:68
     296msgid "My S3 Backup"
     297msgstr ""
     298
     299#: includes/Admin/views/media-tracker.php:71
     300msgid "Provider"
     301msgstr ""
     302
     303#: includes/Admin/views/media-tracker.php:73
     304msgid "Google Drive"
     305msgstr ""
     306
     307#: includes/Admin/views/media-tracker.php:74
     308msgid "Amazon S3"
     309msgstr ""
     310
     311#: includes/Admin/views/media-tracker.php:75
     312msgid "Dropbox"
     313msgstr ""
     314
     315#: includes/Admin/views/media-tracker.php:79
     316msgid "Root Folder / Bucket"
     317msgstr ""
     318
     319#: includes/Admin/views/media-tracker.php:80
     320msgid "/MediaTrackerPro/backup or media-tracker-pro"
     321msgstr ""
     322
     323#: includes/Admin/views/media-tracker.php:83
     324msgid "Region / Location"
     325msgstr ""
     326
     327#: includes/Admin/views/media-tracker.php:84
     328msgid "us-east-1, europe-west1 etc."
     329msgstr ""
     330
     331#: includes/Admin/views/media-tracker.php:86
     332msgid "Production credentials (Access Key, Secret Key) are handled via the WordPress settings page. This modal only previews UI and flow."
     333msgstr ""
     334
     335#: includes/Admin/views/media-tracker.php:89
     336msgid "Cancel"
     337msgstr ""
     338
     339#: includes/Admin/views/media-tracker.php:90
     340msgid "Test Connection"
     341msgstr ""
     342
     343#: includes/Admin/views/media-tracker.php:91
     344msgid "Save Connection"
     345msgstr ""
     346
     347#: includes/Admin/views/tabs/tab-documents.php:15
     348#: includes/functions.php:82
     349msgid "Documents"
     350msgstr ""
     351
     352#: includes/Admin/views/tabs/tab-documents.php:17
     353msgid "Manage and track your document files."
     354msgstr ""
     355
     356#: includes/Admin/views/tabs/tab-documents.php:29
     357msgid "Documentation"
     358msgstr ""
     359
     360#: includes/Admin/views/tabs/tab-documents.php:31
     361msgid "Learn how to track media usage and clean up unused files in WordPress."
     362msgstr ""
     363
     364#: includes/Admin/views/tabs/tab-documents.php:35
     365msgid "Read More"
     366msgstr ""
     367
     368#: includes/Admin/views/tabs/tab-documents.php:44
     369msgid "Join Our Community"
     370msgstr ""
     371
     372#: includes/Admin/views/tabs/tab-documents.php:46
     373msgid "Join our community to discuss features, share ideas, and get help from other users."
     374msgstr ""
     375
     376#: includes/Admin/views/tabs/tab-documents.php:49
     377msgid "Join Now"
     378msgstr ""
     379
     380#: includes/Admin/views/tabs/tab-documents.php:58
     381msgid "Show your love"
     382msgstr ""
     383
     384#: includes/Admin/views/tabs/tab-documents.php:60
     385msgid "Enjoying Media Tracker? Please rate us on WordPress.org to help us grow."
     386msgstr ""
     387
     388#: includes/Admin/views/tabs/tab-documents.php:63
     389msgid "Rate Us"
     390msgstr ""
     391
     392#: includes/Admin/views/tabs/tab-documents.php:71
     393msgid "Video Tutorials"
     394msgstr ""
     395
     396#: includes/Admin/views/tabs/tab-documents.php:77
     397#: includes/Admin/views/tabs/tab-documents.php:101
     398msgid "Getting Started"
     399msgstr ""
     400
     401#: includes/Admin/views/tabs/tab-documents.php:87
     402msgid "Getting Started with Media Tracker"
     403msgstr ""
     404
     405#: includes/Admin/views/tabs/tab-documents.php:90
     406msgid "Learn the basics of Media Tracker in 2 minutes. Watch our comprehensive guide to master file management."
     407msgstr ""
     408
     409#: includes/Admin/views/tabs/tab-duplicates.php:90
     410#: includes/functions.php:42
     411msgid "Duplicate Media"
     412msgstr ""
     413
     414#: includes/Admin/views/tabs/tab-duplicates.php:92
     415msgid "Same hash, probable duplicate images grouped together. Use delete to remove selected images."
     416msgstr ""
     417
     418#: includes/Admin/views/tabs/tab-duplicates.php:125
     419msgid "Scan Duplicates"
     420msgstr ""
     421
     422#: includes/Admin/views/tabs/tab-duplicates.php:135
     423msgid "Thumbnail"
     424msgstr ""
     425
     426#: includes/Admin/views/tabs/tab-duplicates.php:196
     427#: includes/Admin/views/tabs/tab-overview.php:249
     428msgid "Delete"
     429msgstr ""
     430
     431#: includes/Admin/views/tabs/tab-duplicates.php:204
     432msgid "Delete Selected"
     433msgstr ""
     434
     435#: includes/Admin/views/tabs/tab-duplicates.php:211
     436msgid "No duplicate images found."
     437msgstr ""
     438
     439#: includes/Admin/views/tabs/tab-duplicates.php:214
     440msgid "Duplicate images handler not available."
     441msgstr ""
     442
     443#: includes/Admin/views/tabs/tab-external-storage.php:18
     444msgid "Cloud Storage & Automatic Offloading"
     445msgstr ""
     446
     447#: includes/Admin/views/tabs/tab-external-storage.php:19
     448msgid "Upgrade to Media Tracker Pro to automatically move and archive unused files to S3, Google Drive, or Dropbox."
     449msgstr ""
     450
     451#: includes/Admin/views/tabs/tab-external-storage.php:21
     452msgid "Connect Google Drive, S3, Dropbox"
     453msgstr ""
     454
     455#: includes/Admin/views/tabs/tab-external-storage.php:22
     456msgid "Auto-offload unused media"
     457msgstr ""
     458
     459#: includes/Admin/views/tabs/tab-external-storage.php:23
     460msgid "Archive old media automatically"
     461msgstr ""
     462
     463#: includes/Admin/views/tabs/tab-external-storage.php:24
     464msgid "Backup before permanent delete"
     465msgstr ""
     466
     467#: includes/Admin/views/tabs/tab-external-storage.php:25
     468msgid "Storage limit alerts"
     469msgstr ""
     470
     471#: includes/Admin/views/tabs/tab-external-storage.php:26
     472msgid "Restore from cloud anytime"
     473msgstr ""
     474
     475#: includes/Admin/views/tabs/tab-multisite.php:18
     476msgid "Multi-site Network Management"
     477msgstr ""
     478
     479#: includes/Admin/views/tabs/tab-multisite.php:19
     480msgid "Upgrade to Media Tracker Pro to manage and track media from multiple WordPress sites in one central dashboard."
     481msgstr ""
     482
     483#: includes/Admin/views/tabs/tab-multisite.php:21
     484msgid "Connect unlimited network sites"
     485msgstr ""
     486
     487#: includes/Admin/views/tabs/tab-multisite.php:22
     488msgid "Network-wide media tracking"
     489msgstr ""
     490
     491#: includes/Admin/views/tabs/tab-multisite.php:23
     492msgid "Cross-site duplicate detection"
     493msgstr ""
     494
     495#: includes/Admin/views/tabs/tab-multisite.php:24
     496msgid "Centralized storage overview"
     497msgstr ""
     498
     499#: includes/Admin/views/tabs/tab-multisite.php:25
     500msgid "Per-site cleanup rules"
     501msgstr ""
     502
     503#: includes/Admin/views/tabs/tab-multisite.php:26
     504msgid "Network admin controls"
     505msgstr ""
     506
     507#: includes/Admin/views/tabs/tab-optimization.php:18
     508msgid "Advanced Image Optimization"
     509msgstr ""
     510
     511#: includes/Admin/views/tabs/tab-optimization.php:19
     512msgid "Upgrade to Media Tracker Pro to access powerful optimization tools including compression, WebP conversion, and lazy loading."
     513msgstr ""
     514
     515#: includes/Admin/views/tabs/tab-optimization.php:21
     516msgid "Lossless image compression"
     517msgstr ""
     518
     519#: includes/Admin/views/tabs/tab-optimization.php:22
     520msgid "Automatic WebP conversion"
     521msgstr ""
     522
     523#: includes/Admin/views/tabs/tab-optimization.php:23
     524msgid "Smart lazy loading"
     525msgstr ""
     526
     527#: includes/Admin/views/tabs/tab-optimization.php:24
     528msgid "Bulk optimization queue"
     529msgstr ""
     530
     531#: includes/Admin/views/tabs/tab-optimization.php:25
     532msgid "Fallback for old browsers"
     533msgstr ""
     534
     535#: includes/Admin/views/tabs/tab-optimization.php:26
     536msgid "Quality control settings"
     537msgstr ""
     538
     539#: includes/Admin/views/tabs/tab-overview.php:145
     540#: includes/functions.php:26
     541msgid "Dashboard"
     542msgstr ""
     543
     544#: includes/Admin/views/tabs/tab-overview.php:147
     545msgid "Manage unused media, duplicate, optimization and cloud offload from a clean dashboard with Media Tracker."
     546msgstr ""
     547
     548#: includes/Admin/views/tabs/tab-overview.php:157
     549#: includes/Admin/views/unused-media-list.php:21
     550#: includes/functions.php:34
     551msgid "Unused Media"
     552msgstr ""
     553
     554#. translators: %d: Number of unused files.
     555#. translators: %d: Number of duplicate files.
     556#. translators: %d: Total number of media files.
     557#: includes/Admin/views/tabs/tab-overview.php:162
     558#: includes/Admin/views/tabs/tab-overview.php:187
     559#: includes/Admin/views/tabs/tab-overview.php:204
     560#, php-format
     561msgid "%d Files"
     562msgstr ""
     563
     564#. translators: %s: Formatted file size (e.g. 1.5 MB).
     565#: includes/Admin/views/tabs/tab-overview.php:170
     566#, php-format
     567msgid "Potential saving: %s"
     568msgstr ""
     569
     570#: includes/Admin/views/tabs/tab-overview.php:179
     571msgid "Duplicates Found"
     572msgstr ""
     573
     574#: includes/Admin/views/tabs/tab-overview.php:184
     575msgid "Scan Required"
     576msgstr ""
     577
     578#: includes/Admin/views/tabs/tab-overview.php:192
     579msgid "Based on file hash matching"
     580msgstr ""
     581
     582#: includes/Admin/views/tabs/tab-overview.php:199
     583msgid "Total Media"
     584msgstr ""
     585
     586#: includes/Admin/views/tabs/tab-overview.php:207
     587msgid "Total files in library"
     588msgstr ""
     589
     590#: includes/Admin/views/tabs/tab-overview.php:215
     591msgid "Quick Actions"
     592msgstr ""
     593
     594#: includes/Admin/views/tabs/tab-overview.php:220
     595msgid "Scan for Unused Media"
     596msgstr ""
     597
     598#: includes/Admin/views/tabs/tab-overview.php:221
     599msgid "Scan all content to find unused media files."
     600msgstr ""
     601
     602#: includes/Admin/views/tabs/tab-overview.php:224
     603msgid "Scan"
     604msgstr ""
     605
     606#: includes/Admin/views/tabs/tab-overview.php:230
     607msgid "Find Duplicates"
     608msgstr ""
     609
     610#: includes/Admin/views/tabs/tab-overview.php:231
     611msgid "Detects duplicate images using file hash matching."
     612msgstr ""
     613
     614#: includes/Admin/views/tabs/tab-overview.php:234
     615msgid "Find"
     616msgstr ""
     617
     618#: includes/Admin/views/tabs/tab-overview.php:240
     619msgid "Bulk Delete Unused"
     620msgstr ""
     621
     622#. translators: %d: Number of unused files.
     623#: includes/Admin/views/tabs/tab-overview.php:244
     624#, php-format
     625msgid "%d unused files found. Delete safely after backup."
     626msgstr ""
     627
     628#: includes/Admin/views/tabs/tab-overview.php:257
     629msgid "Media Statistics"
     630msgstr ""
     631
     632#. translators: %d: Number of files for a specific mime type.
     633#: includes/Admin/views/tabs/tab-overview.php:279
     634#, php-format
     635msgid "%d files"
     636msgstr ""
     637
     638#: includes/Admin/views/tabs/tab-overview.php:287
     639msgid "No media files found yet."
     640msgstr ""
     641
     642#: includes/Admin/views/tabs/tab-overview.php:297
     643msgid "Recent Unused Media"
     644msgstr ""
     645
     646#: includes/Admin/views/tabs/tab-overview.php:303
     647#: includes/Admin/views/tabs/tab-overview.php:353
     648msgid "File Name"
     649msgstr ""
     650
     651#: includes/Admin/views/tabs/tab-overview.php:339
     652msgid "No unused media found! Great job keeping your library clean."
     653msgstr ""
     654
     655#: includes/Admin/views/tabs/tab-overview.php:347
     656msgid "Most Used Media"
     657msgstr ""
     658
     659#: includes/Admin/views/tabs/tab-overview.php:355
     660msgid "Usage Count"
     661msgstr ""
     662
     663#. translators: %d: Number of times the media is used.
     664#: includes/Admin/views/tabs/tab-overview.php:373
     665#, php-format
     666msgid "%d times"
     667msgstr ""
     668
     669#: includes/Admin/views/tabs/tab-overview.php:390
     670msgid "No media usage data available yet."
     671msgstr ""
     672
     673#: includes/Admin/views/tabs/tab-security.php:18
     674msgid "Security & Activity Logs"
     675msgstr ""
     676
     677#: includes/Admin/views/tabs/tab-security.php:19
     678msgid "Upgrade to Media Tracker Pro for role-based permissions, activity logging, and complete audit trails for all sensitive actions."
     679msgstr ""
     680
     681#: includes/Admin/views/tabs/tab-security.php:21
     682msgid "Role-based access control"
     683msgstr ""
     684
     685#: includes/Admin/views/tabs/tab-security.php:22
     686msgid "Full activity audit logs"
     687msgstr ""
     688
     689#: includes/Admin/views/tabs/tab-security.php:23
     690msgid "Bulk delete confirmation"
     691msgstr ""
     692
     693#: includes/Admin/views/tabs/tab-security.php:24
     694msgid "File whitelist protection"
     695msgstr ""
     696
     697#: includes/Admin/views/tabs/tab-security.php:25
     698msgid "Export logs to CSV"
     699msgstr ""
     700
     701#: includes/Admin/views/tabs/tab-security.php:26
     702msgid "User action tracking"
     703msgstr ""
     704
     705#: includes/Admin/views/tabs/tab-unused-media.php:51
     706msgid "Unused media list class not found."
     707msgstr ""
     708
     709#: includes/Admin/views/unused-media-list.php:23
     710msgid "Detect media files not used anywhere on your site for safe cleanup."
     711msgstr ""
     712
     713#: includes/Admin/views/unused-media-list.php:33
     714msgid "unused media found!"
     715msgstr ""
     716
     717#: includes/Admin/views/unused-media-list.php:39
    242718msgid "Scan status: Ready to scan..."
    243719msgstr ""
    244720
    245 #: includes/Installer.php:104
     721#: includes/Assets.php:38
     722msgid "Image hashes will be refreshed and all images will be re-scanned. Continue?"
     723msgstr ""
     724
     725#: includes/Assets.php:39
     726msgid "Re-scanning..."
     727msgstr ""
     728
     729#: includes/Assets.php:40
     730msgid "Error re-scanning images"
     731msgstr ""
     732
     733#: includes/Cron_Schedules.php:30
     734msgid "Every 5 Minutes"
     735msgstr ""
     736
     737#: includes/functions.php:50
     738msgid "External Storage"
     739msgstr ""
     740
     741#: includes/functions.php:58
     742msgid "Optimization"
     743msgstr ""
     744
     745#: includes/functions.php:65
     746msgid "Security & Logs"
     747msgstr ""
     748
     749#: includes/functions.php:73
     750msgid "Multi-site"
     751msgstr ""
     752
     753#: includes/functions.php:90
     754msgid "Go Pro"
     755msgstr ""
     756
     757#. translators: %s template file path
     758#: includes/functions.php:222
     759#, php-format
     760msgid "%s does not exist."
     761msgstr ""
     762
     763#: includes/functions.php:406
     764msgid "This feature is available in Media Tracker Pro."
     765msgstr ""
     766
     767#: includes/functions.php:425
     768msgid "Upgrade to Media Tracker Pro"
     769msgstr ""
     770
     771#: includes/Installer.php:144
    246772msgid "Media Tracker Plugin Feedback"
    247773msgstr ""
    248774
    249 #: includes/Installer.php:122
     775#: includes/Installer.php:162
    250776msgid "If you have a moment, we'd love to know why you're deactivating the Media Tracker plugin!"
    251777msgstr ""
    252778
    253 #: includes/Installer.php:126
     779#: includes/Installer.php:166
    254780msgid "Enter your feedback here..."
    255781msgstr ""
    256782
    257 #: includes/Installer.php:130
     783#: includes/Installer.php:170
    258784msgid "Skip & Deactivate"
    259785msgstr ""
    260786
    261 #: includes/Installer.php:131
     787#: includes/Installer.php:171
    262788msgid "Submit & Deactivate"
    263789msgstr ""
    264 
    265 #: media-tracker.php:110
    266 msgid "Image hashes will be refreshed and all images will be re-scanned. Continue?"
    267 msgstr ""
    268 
    269 #: media-tracker.php:111
    270 msgid "Re-scanning..."
    271 msgstr ""
    272 
    273 #: media-tracker.php:112
    274 msgid "Error re-scanning images"
    275 msgstr ""
  • media-tracker/trunk/media-tracker.php

    r3441211 r3454648  
    55 * Author: TheBitCraft
    66 * Author URI: https://thebitcraft.com/
    7  * Version: 1.2.2
     7 * Version: 1.3.0
    88 * Requires PHP: 7.4
    99 * Requires at least: 5.9
     
    2828     * @var string
    2929     */
    30     const version = '1.2.2';
     30    const version = '1.3.0';
    3131
    3232    /**
     
    3636        $this->define_constants();
    3737        register_activation_hook( __FILE__, array( $this, 'activate' ) );
     38        register_deactivation_hook( __FILE__, array( '\Media_Tracker\Installer', 'clear_cron_jobs' ) );
    3839        add_action( 'plugins_loaded', array( $this, 'init_plugin' ) );
    39         add_action( 'admin_enqueue_scripts', array( $this, 'admin_script' ) );
    4040        add_action( 'wp_ajax_mt_save_feedback', array( '\Media_Tracker\Installer', 'save_feedback' ) );
    4141        add_action( 'current_screen', function( $screen ) {
     
    9191    public function init_plugin() {
    9292        new Media_Tracker\Media_Tracker_i18n();
    93 
    94         if ( is_admin() ) {
     93        new Media_Tracker\Assets();
     94        new Media_Tracker\Cron_Schedules();
     95        // Load Admin class in admin dashboard OR during cron execution
     96        if ( is_admin() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
    9597            new Media_Tracker\Admin();
    9698        }
    97     }
    98 
    99     /**
    100      * Register necessary CSS and JS
    101      * @ Admin
    102      */
    103     public function admin_script() {
    104         wp_enqueue_style( 'mt-admin-style', MEDIA_TRACKER_URL . '/assets/dist/css/mt-admin.css', false, MEDIA_TRACKER_VERSION );
    105         wp_enqueue_script( 'mt-admin-script', MEDIA_TRACKER_URL . '/assets/dist/js/mt-admin.js', array( 'jquery' ), MEDIA_TRACKER_VERSION, true );
    106         wp_localize_script( 'mt-admin-script', 'mediaTracker', array(
    107             'ajax_url' => admin_url( 'admin-ajax.php' ),
    108             'nonce'    => wp_create_nonce( 'media_tracker_nonce' ),
    109             'i18n'     => array(
    110                 'rescan_confirm' => __( 'Image hashes will be refreshed and all images will be re-scanned. Continue?', 'media-tracker' ),
    111                 'rescanning'     => __( 'Re-scanning...', 'media-tracker' ),
    112                 'rescan_error'   => __( 'Error re-scanning images', 'media-tracker' ),
    113             ),
    114         ) );
    11599    }
    116100}
  • media-tracker/trunk/readme.txt

    r3441211 r3454648  
    66Tested up to: 6.9
    77Requires PHP: 7.4
    8 Stable tag: 1.2.2
     8Stable tag: 1.3.0
    99License: GPLv2 or later
    1010License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    1515== Description ==
    1616Media Tracker is a powerful WordPress plugin designed to help you identify and remove unused media files, manage duplicate images, and streamline your media library for better site performance and storage efficiency. Boost your WordPress site’s speed and organization with Media Tracker, the ultimate solution for managing and optimizing media files. Effortlessly track, organize, and clean up unused images to maintain an efficient and clutter-free media library. With Media Tracker, you can easily locate where each image is used across posts, pages, and custom post types, enhancing your website's performance and user experience.
     17
     18[youtube https://www.youtube.com/watch?v=2eMRuW5X-iI]
    1719
    1820## Features
     
    5759
    5860== Screenshots ==
    59 1. Example of media tracking report.
    60 2. Unused media cleaner interface.
    61 3. Find dupliacte image
     611. Media Tracker Dashboard
     622. Example of media tracking report.
     633. Unused media cleaner interface.
     644. Find dupliacte image
     655. Documentations
    6266
    6367== Changelog ==
     68= 1.3.0 [05/02/2026] =
     69* New: Complete design overhaul with a modern, unified Dashboard interface
     70* New: Consolidated all tools (Unused Media, Duplicates) into a single "Media Tracker" page with tabbed navigation
     71* New: "Overview" tab providing a high-level summary of library usage and stats
     72* New: Dedicated sections for Pro features (Optimization, Security, External Storage, Multi-site)
     73* New: "Remove All" button added to Unused Media scanner for bulk cleanup
     74* New: "Documents" tab for managing document with video tutorials
     75* Enhanced: Rebuilt stylesheets using SCSS for better maintainability and consistency
     76* Enhanced: Redesigned progress bars for Unused Media and Duplicate scans
     77* Enhanced: Improved Media Usage table layout and responsiveness
     78* Fixed: Critical issue with Scan buttons not responding in some scenarios
     79* Fixed: Tab navigation state lost on page reload (URL handling fixed)
     80* Fixed: "Direct DB Query" warnings by optimizing database calls
     81* Fixed: PHPCS compliance issues (variable prefixing, escaping)
     82* Fixed: Cron schedule registration to prevent "invalid_schedule" errors
     83* Fixed: Overview tab unused media count accuracy
     84* Internal: Codebase improvements and optimization
     85
    6486= 1.2.2 [17/01/2026] =
    6587* Fixed: Deactivation feedback form now correctly sends email and deactivates plugin
  • media-tracker/trunk/vendor/composer/InstalledVersions.php

    r3140549 r3454648  
    2828{
    2929    /**
     30     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
     31     * @internal
     32     */
     33    private static $selfDir = null;
     34
     35    /**
    3036     * @var mixed[]|null
    3137     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
    3238     */
    3339    private static $installed;
     40
     41    /**
     42     * @var bool
     43     */
     44    private static $installedIsLocalDir;
    3445
    3546    /**
     
    310321        self::$installed = $data;
    311322        self::$installedByVendor = array();
     323
     324        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
     325        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
     326        // so we have to assume it does not, and that may result in duplicate data being returned when listing
     327        // all installed packages for example
     328        self::$installedIsLocalDir = false;
     329    }
     330
     331    /**
     332     * @return string
     333     */
     334    private static function getSelfDir()
     335    {
     336        if (self::$selfDir === null) {
     337            self::$selfDir = strtr(__DIR__, '\\', '/');
     338        }
     339
     340        return self::$selfDir;
    312341    }
    313342
     
    323352
    324353        $installed = array();
     354        $copiedLocalDir = false;
    325355
    326356        if (self::$canGetVendors) {
     357            $selfDir = self::getSelfDir();
    327358            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
     359                $vendorDir = strtr($vendorDir, '\\', '/');
    328360                if (isset(self::$installedByVendor[$vendorDir])) {
    329361                    $installed[] = self::$installedByVendor[$vendorDir];
     
    331363                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
    332364                    $required = require $vendorDir.'/composer/installed.php';
    333                     $installed[] = self::$installedByVendor[$vendorDir] = $required;
    334                     if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
    335                         self::$installed = $installed[count($installed) - 1];
     365                    self::$installedByVendor[$vendorDir] = $required;
     366                    $installed[] = $required;
     367                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
     368                        self::$installed = $required;
     369                        self::$installedIsLocalDir = true;
    336370                    }
     371                }
     372                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
     373                    $copiedLocalDir = true;
    337374                }
    338375            }
     
    351388        }
    352389
    353         if (self::$installed !== array()) {
     390        if (self::$installed !== array() && !$copiedLocalDir) {
    354391            $installed[] = self::$installed;
    355392        }
  • media-tracker/trunk/vendor/composer/autoload_real.php

    r3151282 r3454648  
    3232        $loader->register(true);
    3333
     34        $filesToLoad = \Composer\Autoload\ComposerStaticInit27ae33a58e56550f0fd0ae9ff16605b6::$files;
     35        $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
     36            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
     37                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
     38
     39                require $file;
     40            }
     41        }, null, null);
     42        foreach ($filesToLoad as $fileIdentifier => $file) {
     43            $requireFile($fileIdentifier, $file);
     44        }
     45
    3446        return $loader;
    3547    }
  • media-tracker/trunk/vendor/composer/autoload_static.php

    r3151282 r3454648  
    77class ComposerStaticInit27ae33a58e56550f0fd0ae9ff16605b6
    88{
     9    public static $files = array (
     10        '5ca4469d9069a9cdada698d89b95c729' => __DIR__ . '/../..' . '/includes/functions.php',
     11    );
     12
    913    public static $prefixLengthsPsr4 = array (
    1014        'M' =>
  • media-tracker/trunk/vendor/composer/installed.php

    r3151282 r3454648  
    44        'pretty_version' => 'dev-main',
    55        'version' => 'dev-main',
    6         'reference' => '8c342a3e99839e4b78d4e710e735baa1877a6580',
     6        'reference' => '274607d53e2c30110379855a4fcfc42dd0045d97',
    77        'type' => 'wordpress-plugin',
    88        'install_path' => __DIR__ . '/../../',
     
    1414            'pretty_version' => 'dev-main',
    1515            'version' => 'dev-main',
    16             'reference' => '8c342a3e99839e4b78d4e710e735baa1877a6580',
     16            'reference' => '274607d53e2c30110379855a4fcfc42dd0045d97',
    1717            'type' => 'wordpress-plugin',
    1818            'install_path' => __DIR__ . '/../../',
Note: See TracChangeset for help on using the changeset viewer.