feat: Implement the Instagram Gallery Sync Pro WordPress plugin.

This commit is contained in:
2026-01-22 00:15:58 +01:00
commit f2ee863afe
39 changed files with 8772 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

279
README.md Normal file
View File

@@ -0,0 +1,279 @@
# Instagram Gallery Sync Pro
[![WordPress](https://img.shields.io/badge/WordPress-5.0%2B-blue.svg)](https://wordpress.org/)
[![PHP](https://img.shields.io/badge/PHP-7.4%2B-purple.svg)](https://php.net/)
[![License](https://img.shields.io/badge/License-GPL--2.0%2B-green.svg)](https://www.gnu.org/licenses/gpl-2.0.html)
Ein leistungsstarkes WordPress-Plugin zur Synchronisierung und Anzeige von Instagram-Fotos ohne offizielle API.
**Entwickelt von [Keil & Schick](https://keil-schick.de)**
---
## 🚀 Features
- **Automatische Synchronisierung** Holt regelmäßig neue Bilder von öffentlichen Instagram-Profilen
- **Lokale Bildspeicherung** Bilder werden auf deinem Server gespeichert (bessere Performance)
- **5 Layout-Optionen** Grid, Masonry, Justified, Slider, List
- **Vollständig responsive** Separate Spalteneinstellungen für Desktop, Tablet & Mobile
- **Lightbox-Integration** GLightbox für elegante Bildanzeige
- **Gutenberg-Block** Nativer Block-Editor mit Live-Vorschau
- **Shortcode-Unterstützung** Flexibel mit zahlreichen Parametern
- **6 Hover-Effekte** Zoom, Fade, Overlay, Grayscale, Lift, None
- **Custom CSS** Eigene Styles direkt im Admin-Bereich
- **Activity Logs** Detaillierte Protokollierung aller Sync-Aktivitäten
- **i18n-ready** Vorbereitet für Übersetzungen (DE/EN)
---
## 📋 Anforderungen
- WordPress 5.0 oder höher
- PHP 7.4 oder höher
- Öffentliches Instagram-Profil
---
## 📥 Installation
### Manuelle Installation
1. Lade den `instagram-gallery-sync-pro` Ordner herunter
2. Kopiere ihn nach `/wp-content/plugins/`
3. Aktiviere das Plugin unter **Plugins** im WordPress-Admin
4. Gehe zu **Instagram Gallery** in der Sidebar
### Via WordPress Admin
1. Gehe zu **Plugins → Installieren**
2. Klicke auf **Plugin hochladen**
3. Wähle die ZIP-Datei aus
4. Klicke auf **Jetzt installieren** und dann **Aktivieren**
---
## ⚙️ Konfiguration
### Grundeinrichtung
1. Navigiere zu **Instagram Gallery** im Admin-Menü
2. Gib deinen Instagram-Benutzernamen ein (ohne @)
3. Wähle die gewünschte Bildanzahl und Qualität
4. Klicke auf **Sync Now**
### Einstellungs-Tabs
| Tab | Beschreibung |
|-----|--------------|
| **Instagram** | Username, Sync-Intervall, Bildqualität |
| **Layout** | Grid-Typ, Spalten, Abstände, Hover-Effekte |
| **Anzeige** | Limit, Sortierung, Lightbox, Lazy Loading |
| **Styling** | Farben, Schriftgrößen, Custom CSS |
| **Erweitert** | Debug-Modus, Cache, Proxy, Logs |
---
## 🔧 Verwendung
### Shortcode
Einfachste Verwendung:
```
[instagram_gallery]
```
Mit Parametern:
```
[instagram_gallery layout="masonry" columns="4" limit="12" spacing="15"]
```
#### Alle Parameter
| Parameter | Werte | Standard |
|-----------|-------|----------|
| `layout` | `grid`, `masonry`, `slider`, `justified`, `list` | `grid` |
| `columns` | `1` - `6` | `3` |
| `spacing` | `0` - `50` (px) | `10` |
| `limit` | `1` - `50` | `12` |
| `order` | `newest`, `oldest`, `random` | `newest` |
| `lightbox` | `yes`, `no` | `yes` |
| `captions` | `yes`, `no` | `no` |
| `autoplay` | `true`, `false` (nur Slider) | `false` |
| `class` | CSS-Klassenname | |
### Gutenberg Block
1. Öffne den Block-Editor
2. Suche nach "Instagram Gallery"
3. Füge den Block hinzu
4. Konfiguriere über die Sidebar-Einstellungen
### PHP Template
```php
<?php
if (function_exists('display_instagram_gallery')) {
display_instagram_gallery(array(
'layout' => 'grid',
'columns' => 3,
'limit' => 9
));
}
?>
```
---
## 🎨 Styling
### CSS-Variablen
Das Plugin verwendet CSS Custom Properties für einfache Anpassung:
```css
:root {
--igsp-primary: #e1306c;
--igsp-hover: #c13584;
--igsp-text: #ffffff;
--igsp-font-size: 14px;
--igsp-radius: 0px;
}
```
### CSS-Klassen
| Klasse | Beschreibung |
|--------|--------------|
| `.igsp-gallery` | Container |
| `.igsp-item` | Einzelnes Bild |
| `.igsp-image` | Bild-Element |
| `.igsp-overlay` | Caption-Overlay |
| `.igsp-follow-btn` | Instagram-Button |
---
## 🔄 Synchronisierung
### Automatisch
Das Plugin synchronisiert automatisch basierend auf dem konfigurierten Intervall:
- Alle 30 Minuten
- Stündlich
- Alle 6 Stunden
- Täglich
- Wöchentlich
### Manuell
Klicke auf **Sync Now** im Instagram-Tab der Einstellungen.
### Scraping-Methoden
Das Plugin verwendet mehrere Fallback-Methoden:
1. **Web Profile Info API** Instagrams interne Web-API
2. **Embed Page Parsing** Parst die Embed-Seiten
3. **Profile Page Parsing** HTML-Analyse mit mehreren Strategien
---
## ⚠️ Wichtige Hinweise
> **Hinweis:** Instagram blockiert Scraping aktiv. Das Plugin enthält Anti-Blocking-Maßnahmen, aber 100% Zuverlässigkeit kann nicht garantiert werden.
### Best Practices
- Verwende **lange Sync-Intervalle** (6h+)
- Aktiviere **lokale Bildspeicherung**
- Nutze **Caching** für bessere Performance
- Halte die **Bildanzahl** moderat (12-24)
### Problembehandlung
| Problem | Lösung |
|---------|--------|
| Keine Bilder | Prüfe ob Profil öffentlich ist |
| Rate Limiting | Erhöhe Sync-Intervall |
| Timeout | Erhöhe Request-Timeout in Erweitert |
| Parsing-Fehler | Prüfe Logs, warte und versuche erneut |
---
## 🗄️ Datenbank
Das Plugin erstellt zwei Tabellen:
- `wp_instagram_gallery_posts` Gespeicherte Bilder
- `wp_instagram_gallery_log` Aktivitätsprotokolle
Bei Deinstallation werden alle Daten vollständig entfernt.
---
## 📁 Dateistruktur
```
instagram-gallery-sync-pro/
├── instagram-gallery-sync-pro.php # Hauptdatei
├── uninstall.php # Deinstallations-Routine
├── includes/
│ ├── class-admin.php # Admin-Handler
│ ├── class-cron.php # Cron-Jobs
│ ├── class-database.php # Datenbank-Operationen
│ ├── class-gutenberg-block.php # Block-Editor
│ ├── class-image-handler.php # Bild-Download & -Verarbeitung
│ ├── class-logger.php # Logging-System
│ ├── class-scraper.php # Instagram-Scraper
│ └── class-shortcode.php # Shortcode-Handler
├── admin/
│ ├── css/admin-style.css
│ ├── js/admin-script.js
│ ├── js/ajax-sync.js
│ └── views/ # Admin-Templates
├── public/
│ ├── css/ # Frontend-Styles
│ └── js/ # Frontend-Scripts + Libs
└── languages/ # Übersetzungsdateien
```
---
## 🔐 Sicherheit
- Prepared Statements für alle DB-Queries
- Nonce-Verifizierung für AJAX-Requests
- Capability-Checks (`manage_options`)
- Input-Sanitization für alle Benutzereingaben
- Output-Escaping mit `esc_html()`, `esc_attr()`, etc.
---
## 📝 Changelog
### 1.0.0 (2026-01-22)
- Erste Veröffentlichung
- 5 Layout-Typen (Grid, Masonry, Justified, Slider, List)
- Gutenberg Block mit Live-Vorschau
- Shortcode mit allen Parametern
- Admin-Bereich mit 5 Tabs
- Multi-Methoden Scraping Engine
- Lokale Bildspeicherung mit Thumbnails
- Activity Logging System
---
## 📄 Lizenz
GPL-2.0-or-later © [Keil & Schick](https://keil-schick.de)
---
## 🆘 Support
Für Fragen und Support besuche: [https://keil-schick.de](https://keil-schick.de)
---
*Entwickelt mit ❤️ in Deutschland*

BIN
admin/.DS_Store vendored Normal file

Binary file not shown.

649
admin/css/admin-style.css Normal file
View File

@@ -0,0 +1,649 @@
/**
* Instagram Gallery Sync Pro - Admin Styles
*
* @package Instagram_Gallery_Sync_Pro
*/
/* ============================================
Variables
============================================ */
:root {
--igsp-primary: #e1306c;
--igsp-primary-hover: #c13584;
--igsp-success: #28a745;
--igsp-warning: #ffc107;
--igsp-error: #dc3545;
--igsp-info: #17a2b8;
--igsp-dark: #1e1e1e;
--igsp-gray: #6c757d;
--igsp-light: #f8f9fa;
--igsp-border: #e2e4e7;
--igsp-shadow: 0 2px 8px rgba(0,0,0,0.08);
--igsp-radius: 8px;
}
/* ============================================
Layout
============================================ */
.igsp-admin-wrap {
display: grid;
grid-template-columns: 1fr 300px;
gap: 20px;
margin: 20px 20px 20px 0;
}
.igsp-admin-header {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: linear-gradient(135deg, var(--igsp-primary), var(--igsp-primary-hover));
color: #fff;
border-radius: var(--igsp-radius);
margin-bottom: 10px;
}
.igsp-admin-header h1 {
margin: 0;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
font-size: 24px;
}
.igsp-admin-header .dashicons {
font-size: 30px;
width: 30px;
height: 30px;
}
.igsp-version {
margin: 0;
opacity: 0.8;
font-size: 14px;
}
.igsp-admin-content {
background: #fff;
border-radius: var(--igsp-radius);
box-shadow: var(--igsp-shadow);
overflow: hidden;
}
/* ============================================
Tabs Navigation
============================================ */
.igsp-tabs-nav {
display: flex;
background: var(--igsp-light);
border-bottom: 1px solid var(--igsp-border);
padding: 0 20px;
gap: 5px;
}
.igsp-tab-link {
display: flex;
align-items: center;
gap: 8px;
padding: 15px 20px;
text-decoration: none;
color: var(--igsp-gray);
font-weight: 500;
border-bottom: 3px solid transparent;
margin-bottom: -1px;
transition: all 0.2s ease;
}
.igsp-tab-link:hover {
color: var(--igsp-primary);
}
.igsp-tab-link.active {
color: var(--igsp-primary);
border-bottom-color: var(--igsp-primary);
background: #fff;
}
.igsp-tab-link .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
/* ============================================
Tab Content
============================================ */
.igsp-tab-content {
padding: 30px;
}
.igsp-section {
margin-bottom: 40px;
}
.igsp-section:last-child {
margin-bottom: 0;
}
.igsp-section h2 {
font-size: 18px;
font-weight: 600;
color: var(--igsp-dark);
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 2px solid var(--igsp-border);
}
/* ============================================
Form Elements
============================================ */
.igsp-settings-form .form-table th {
padding: 20px 10px 20px 0;
font-weight: 500;
}
.igsp-settings-form .form-table td {
padding: 15px 10px;
}
.igsp-input-wrapper {
display: flex;
align-items: center;
max-width: 300px;
}
.igsp-input-prefix {
background: var(--igsp-light);
border: 1px solid var(--igsp-border);
border-right: none;
padding: 6px 12px;
border-radius: var(--igsp-radius) 0 0 var(--igsp-radius);
color: var(--igsp-gray);
}
.igsp-input-wrapper input {
border-radius: 0 var(--igsp-radius) var(--igsp-radius) 0 !important;
}
/* Slider Wrapper */
.igsp-slider-wrapper {
display: flex;
align-items: center;
gap: 15px;
max-width: 400px;
}
.igsp-slider-wrapper input[type="range"] {
flex: 1;
-webkit-appearance: none;
height: 6px;
background: var(--igsp-border);
border-radius: 3px;
outline: none;
}
.igsp-slider-wrapper input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: var(--igsp-primary);
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
}
.igsp-slider-wrapper input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.igsp-slider-value {
min-width: 50px;
padding: 5px 10px;
background: var(--igsp-light);
border-radius: var(--igsp-radius);
text-align: center;
font-weight: 500;
font-size: 13px;
}
/* Inline Fields */
.igsp-inline-fields {
display: flex;
gap: 10px;
align-items: center;
}
/* ============================================
Layout Selector
============================================ */
.igsp-layout-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 15px;
max-width: 800px;
}
.igsp-layout-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
border: 2px solid var(--igsp-border);
border-radius: var(--igsp-radius);
cursor: pointer;
transition: all 0.2s ease;
background: #fff;
}
.igsp-layout-option:hover {
border-color: var(--igsp-primary);
}
.igsp-layout-option.selected {
border-color: var(--igsp-primary);
background: rgba(225, 48, 108, 0.05);
}
.igsp-layout-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.igsp-layout-preview {
width: 100px;
height: 70px;
display: grid;
gap: 3px;
margin-bottom: 10px;
}
.igsp-layout-preview span {
background: var(--igsp-border);
border-radius: 2px;
}
.igsp-preview-grid {
grid-template-columns: repeat(3, 1fr);
}
.igsp-preview-masonry {
grid-template-columns: repeat(3, 1fr);
}
.igsp-preview-justified {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.igsp-preview-justified span {
height: 30px;
}
.igsp-preview-slider {
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
}
.igsp-preview-slider .slide {
flex: 1;
height: 100%;
}
.igsp-preview-slider .arrow-left,
.igsp-preview-slider .arrow-right {
background: none;
color: var(--igsp-gray);
font-size: 20px;
}
.igsp-preview-list {
grid-template-columns: 1fr;
}
.igsp-layout-name {
font-weight: 500;
color: var(--igsp-dark);
font-size: 13px;
}
/* ============================================
Sidebar
============================================ */
.igsp-admin-sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.igsp-sidebar-box {
background: #fff;
border-radius: var(--igsp-radius);
box-shadow: var(--igsp-shadow);
padding: 20px;
}
.igsp-sidebar-box h3 {
margin: 0 0 15px 0;
font-size: 14px;
font-weight: 600;
color: var(--igsp-dark);
}
/* Status Box */
.igsp-status-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--igsp-border);
}
.igsp-status-row:last-child {
border-bottom: none;
}
.igsp-status-label {
color: var(--igsp-gray);
font-size: 13px;
}
.igsp-status-value {
font-weight: 600;
font-size: 13px;
}
/* Shortcode Box */
.igsp-shortcode-display {
display: block;
padding: 12px;
background: var(--igsp-dark);
color: #fff;
border-radius: var(--igsp-radius);
font-size: 13px;
margin-bottom: 10px;
}
/* Help Box */
.igsp-help-box ul {
margin: 0;
padding: 0;
list-style: none;
}
.igsp-help-box li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
}
.igsp-help-box li .dashicons {
color: var(--igsp-primary);
}
.igsp-help-box a {
text-decoration: none;
}
/* ============================================
Progress Bar
============================================ */
.igsp-progress-wrapper {
margin-top: 15px;
}
.igsp-progress-bar {
height: 8px;
background: var(--igsp-light);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.igsp-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--igsp-primary), var(--igsp-primary-hover));
width: 0%;
transition: width 0.3s ease;
animation: igsp-progress-pulse 1.5s ease-in-out infinite;
}
@keyframes igsp-progress-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.igsp-progress-text {
font-size: 13px;
color: var(--igsp-gray);
}
/* Sync Result */
.igsp-sync-result {
margin-top: 15px;
padding: 12px;
border-radius: var(--igsp-radius);
font-size: 13px;
}
.igsp-sync-result.success {
background: rgba(40, 167, 69, 0.1);
color: var(--igsp-success);
}
.igsp-sync-result.error {
background: rgba(220, 53, 69, 0.1);
color: var(--igsp-error);
}
/* ============================================
Tools Grid
============================================ */
.igsp-tools-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.igsp-tool-card {
padding: 20px;
background: var(--igsp-light);
border-radius: var(--igsp-radius);
}
.igsp-tool-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
}
.igsp-tool-card p {
margin: 0 0 15px 0;
font-size: 13px;
color: var(--igsp-gray);
}
.igsp-tool-danger {
background: rgba(220, 53, 69, 0.05);
}
.button-danger {
background: var(--igsp-error) !important;
border-color: var(--igsp-error) !important;
color: #fff !important;
}
.button-danger:hover {
background: #c82333 !important;
}
/* ============================================
Logs
============================================ */
.igsp-logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.igsp-logs-header h2 {
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
.igsp-logs-table-wrapper {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--igsp-border);
border-radius: var(--igsp-radius);
}
.igsp-logs-table {
margin: 0 !important;
border: none !important;
}
.igsp-logs-table th {
background: var(--igsp-light);
position: sticky;
top: 0;
}
.igsp-log-type { width: 100px; }
.igsp-log-date { width: 120px; }
.igsp-log-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.igsp-log-success { background: rgba(40, 167, 69, 0.1); color: var(--igsp-success); }
.igsp-log-error { background: rgba(220, 53, 69, 0.1); color: var(--igsp-error); }
.igsp-log-warning { background: rgba(255, 193, 7, 0.1); color: #856404; }
.igsp-log-info { background: rgba(23, 162, 184, 0.1); color: var(--igsp-info); }
.igsp-no-logs {
padding: 40px;
text-align: center;
color: var(--igsp-gray);
background: var(--igsp-light);
border-radius: var(--igsp-radius);
}
/* ============================================
Style Preview
============================================ */
.igsp-style-preview {
padding: 30px;
background: var(--igsp-light);
border-radius: var(--igsp-radius);
display: flex;
justify-content: center;
}
.igsp-preview-item {
text-align: center;
}
.igsp-preview-image {
width: 200px;
height: 200px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: var(--igsp-radius);
position: relative;
overflow: hidden;
margin-bottom: 15px;
}
.igsp-preview-overlay {
position: absolute;
inset: 0;
background: var(--hover);
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s;
}
.igsp-preview-image:hover .igsp-preview-overlay {
opacity: 0.8;
}
.igsp-preview-caption {
color: var(--text);
}
.igsp-preview-button {
background: var(--primary) !important;
color: var(--text) !important;
border: none !important;
padding: 10px 25px !important;
border-radius: var(--igsp-radius) !important;
cursor: pointer;
transition: background 0.2s;
}
.igsp-preview-button:hover {
background: var(--hover) !important;
}
/* ============================================
Color Picker
============================================ */
.wp-picker-container {
display: flex;
align-items: center;
gap: 10px;
}
/* ============================================
Responsive
============================================ */
@media (max-width: 1200px) {
.igsp-admin-wrap {
grid-template-columns: 1fr;
}
.igsp-admin-sidebar {
flex-direction: row;
flex-wrap: wrap;
}
.igsp-sidebar-box {
flex: 1;
min-width: 250px;
}
}
@media (max-width: 782px) {
.igsp-tabs-nav {
flex-wrap: wrap;
padding: 10px;
}
.igsp-tab-link {
flex: 1;
justify-content: center;
padding: 10px;
font-size: 12px;
}
.igsp-tab-link .dashicons {
display: none;
}
.igsp-layout-selector {
grid-template-columns: repeat(2, 1fr);
}
}

1
admin/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden

286
admin/js/admin-script.js Normal file
View File

@@ -0,0 +1,286 @@
/**
* Instagram Gallery Sync Pro - Admin Script
*
* Handles admin UI interactions
*
* @package Instagram_Gallery_Sync_Pro
*/
(function($) {
'use strict';
/**
* Admin Controller
*/
const IGSPAdmin = {
/**
* Initialize
*/
init: function() {
this.initColorPickers();
this.initSliders();
this.initLayoutSelector();
this.initConditionalFields();
this.initSyncButton();
this.initToolButtons();
this.initTabPersistence();
},
/**
* Initialize color pickers
*/
initColorPickers: function() {
$('.igsp-color-picker').wpColorPicker({
change: function(event, ui) {
// Update live preview
IGSPAdmin.updatePreview();
}
});
},
/**
* Initialize range sliders
*/
initSliders: function() {
$('input[type="range"]').on('input change', function() {
const $slider = $(this);
const $value = $slider.siblings('.igsp-slider-value');
let value = $slider.val();
// Handle special cases
const id = $slider.attr('id');
if (id === 'igsp_auto_delete_days') {
value = value == 0 ? igspAdmin.strings.never || 'Never' : value + ' days';
} else if (id === 'igsp_spacing' || id === 'igsp_padding' ||
id === 'igsp_border_radius' || id === 'igsp_caption_font_size') {
value += 'px';
} else if (id === 'igsp_request_timeout') {
value += 's';
}
$value.text(value);
// Update live preview if applicable
if (id === 'igsp_caption_font_size') {
IGSPAdmin.updatePreview();
}
});
},
/**
* Initialize layout selector
*/
initLayoutSelector: function() {
$('.igsp-layout-option input').on('change', function() {
$('.igsp-layout-option').removeClass('selected');
$(this).closest('.igsp-layout-option').addClass('selected');
});
},
/**
* Initialize conditional fields
*/
initConditionalFields: function() {
$('[data-depends-on]').each(function() {
const $field = $(this);
const dependsOn = $field.data('depends-on');
const dependsValue = $field.data('depends-value');
function checkVisibility() {
const $input = $('[name="' + dependsOn + '"]:checked, [name="' + dependsOn + '"]').filter(':checked, select');
const currentValue = $input.val();
if (currentValue === dependsValue) {
$field.show();
} else {
$field.hide();
}
}
// Initial check
checkVisibility();
// Listen for changes
$('[name="' + dependsOn + '"]').on('change', checkVisibility);
});
},
/**
* Initialize sync button
*/
initSyncButton: function() {
$('#igsp-sync-now').on('click', function() {
const $button = $(this);
const $progress = $('#igsp-sync-progress');
const $result = $('#igsp-sync-result');
// Disable button
$button.prop('disabled', true);
$progress.show();
$result.hide();
// Animate progress bar
const $fill = $progress.find('.igsp-progress-fill');
$fill.css('width', '0%');
// Simulate progress
let progress = 0;
const progressInterval = setInterval(function() {
progress += Math.random() * 15;
if (progress > 90) progress = 90;
$fill.css('width', progress + '%');
}, 500);
// Make AJAX request
$.ajax({
url: igspAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'igsp_manual_sync',
nonce: igspAdmin.nonce
},
success: function(response) {
clearInterval(progressInterval);
$fill.css('width', '100%');
setTimeout(function() {
$progress.hide();
$result.removeClass('success error');
if (response.success) {
$result.addClass('success').html(response.data.message).show();
IGSPAdmin.updateSyncStatus();
} else {
$result.addClass('error').html(response.data.message || igspAdmin.strings.syncError).show();
}
$button.prop('disabled', false);
}, 500);
},
error: function() {
clearInterval(progressInterval);
$progress.hide();
$result.addClass('error').html(igspAdmin.strings.syncError).show();
$button.prop('disabled', false);
}
});
});
},
/**
* Initialize tool buttons
*/
initToolButtons: function() {
// Clear cache
$('#igsp-clear-cache').on('click', function() {
const $button = $(this);
$button.prop('disabled', true).text(igspAdmin.strings.processing);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_clear_cache',
nonce: igspAdmin.nonce
}, function(response) {
if (response.success) {
$button.text(igspAdmin.strings.done);
setTimeout(function() {
$button.prop('disabled', false).html('<span class="dashicons dashicons-trash"></span> Clear Cache');
}, 2000);
}
});
});
// Reset data
$('#igsp-reset-data').on('click', function() {
if (!confirm(igspAdmin.strings.confirmReset)) {
return;
}
const $button = $(this);
$button.prop('disabled', true).text(igspAdmin.strings.processing);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_reset_data',
nonce: igspAdmin.nonce
}, function(response) {
if (response.success) {
location.reload();
}
});
});
// Clear logs
$('#igsp-clear-logs').on('click', function() {
if (!confirm(igspAdmin.strings.confirmClearLogs)) {
return;
}
const $button = $(this);
$button.prop('disabled', true);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_clear_logs',
nonce: igspAdmin.nonce
}, function(response) {
if (response.success) {
location.reload();
}
});
});
},
/**
* Update sync status
*/
updateSyncStatus: function() {
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_get_sync_status',
nonce: igspAdmin.nonce
}, function(response) {
if (response.success) {
$('#igsp-total-posts').text(response.data.total_posts);
$('#igsp-last-sync').text(response.data.last_sync);
$('#igsp-next-sync').text(response.data.next_sync);
}
});
},
/**
* Update live preview
*/
updatePreview: function() {
const primaryColor = $('#igsp_primary_color').val() || '#e1306c';
const hoverColor = $('#igsp_hover_color').val() || '#c13584';
const textColor = $('#igsp_text_color').val() || '#ffffff';
const fontSize = $('#igsp_caption_font_size').val() || 14;
$('.igsp-preview-item').css({
'--primary': primaryColor,
'--hover': hoverColor,
'--text': textColor
});
$('.igsp-preview-caption').css('font-size', fontSize + 'px');
},
/**
* Tab persistence
*/
initTabPersistence: function() {
// Store current tab in localStorage
$('.igsp-tab-link').on('click', function() {
const tab = $(this).attr('href').split('tab=')[1];
if (tab) {
localStorage.setItem('igsp_active_tab', tab);
}
});
}
};
// Initialize when document is ready
$(document).ready(function() {
IGSPAdmin.init();
});
})(jQuery);

182
admin/js/ajax-sync.js Normal file
View File

@@ -0,0 +1,182 @@
/**
* Instagram Gallery Sync Pro - AJAX Sync Handler
*
* Handles manual sync with progress feedback
*
* @package Instagram_Gallery_Sync_Pro
*/
(function ($) {
'use strict';
/**
* Sync Controller
*/
const IGSPSync = {
isRunning: false,
/**
* Start sync process
*/
start: function () {
if (this.isRunning) {
return;
}
this.isRunning = true;
const $button = $('#igsp-sync-now');
const $progress = $('#igsp-sync-progress');
const $progressFill = $progress.find('.igsp-progress-fill');
const $progressText = $progress.find('.igsp-progress-text');
const $result = $('#igsp-sync-result');
// Reset UI
$button.prop('disabled', true);
$progress.show();
$result.hide().removeClass('success error');
$progressFill.css('width', '0%');
// Start progress animation
this.animateProgress($progressFill, $progressText);
// Execute sync
this.executeSync($button, $progress, $progressFill, $result);
},
/**
* Animate progress bar
*/
animateProgress: function ($fill, $text) {
const stages = [
{ progress: 20, text: 'Connecting to Instagram...' },
{ progress: 40, text: 'Fetching profile data...' },
{ progress: 60, text: 'Downloading images...' },
{ progress: 80, text: 'Saving to database...' },
{ progress: 90, text: 'Finalizing...' }
];
let stageIndex = 0;
this.progressInterval = setInterval(function () {
if (stageIndex < stages.length && IGSPSync.isRunning) {
$fill.css('width', stages[stageIndex].progress + '%');
$text.text(stages[stageIndex].text);
stageIndex++;
}
}, 1500);
},
/**
* Execute sync via AJAX
*/
executeSync: function ($button, $progress, $fill, $result) {
const self = this;
$.ajax({
url: igspAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'igsp_manual_sync',
nonce: igspAdmin.nonce
},
timeout: 120000, // 2 minute timeout
success: function (response) {
self.complete($button, $progress, $fill, $result, response);
},
error: function (xhr, status, error) {
let message = igspAdmin.strings.syncError;
if (status === 'timeout') {
message = 'Sync timed out. The process may still be running in the background.';
} else if (xhr.responseJSON && xhr.responseJSON.data) {
message = xhr.responseJSON.data.message;
}
self.complete($button, $progress, $fill, $result, {
success: false,
data: { message: message }
});
}
});
},
/**
* Complete sync process
*/
complete: function ($button, $progress, $fill, $result, response) {
clearInterval(this.progressInterval);
this.isRunning = false;
// Complete progress bar
$fill.css('width', '100%');
setTimeout(function () {
$progress.hide();
if (response.success) {
$result
.addClass('success')
.html(IGSPSync.formatSuccessMessage(response.data))
.show();
// Update sidebar status
IGSPSync.updateStatus();
} else {
$result
.addClass('error')
.html(response.data.message || igspAdmin.strings.syncError)
.show();
}
$button.prop('disabled', false);
}, 500);
},
/**
* Format success message
*/
formatSuccessMessage: function (data) {
let html = '<strong>✓ ' + igspAdmin.strings.syncComplete + '</strong><br>';
if (data.posts_added > 0) {
html += '<span>Added: ' + data.posts_added + ' new posts</span><br>';
}
if (data.posts_updated > 0) {
html += '<span>Updated: ' + data.posts_updated + ' existing posts</span><br>';
}
if (data.posts_skipped > 0) {
html += '<span>Skipped: ' + data.posts_skipped + ' posts</span>';
}
if (data.errors && data.errors.length > 0) {
html += '<br><small style="color: #dc3545;">Warnings: ' + data.errors.length + '</small>';
}
return html;
},
/**
* Update sync status in sidebar
*/
updateStatus: function () {
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_get_sync_status',
nonce: igspAdmin.nonce
}, function (response) {
if (response.success) {
$('#igsp-total-posts').text(response.data.total_posts);
$('#igsp-last-sync').text(response.data.last_sync);
$('#igsp-next-sync').text(response.data.next_sync);
}
});
}
};
// Make it available globally
window.IGSPSync = IGSPSync;
})(jQuery);

View File

@@ -0,0 +1,122 @@
<?php
/**
* Admin Settings Page Template
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$admin = new IGSP_Admin();
$current_tab = $admin->get_current_tab();
$tabs = $admin->get_tabs();
?>
<div class="wrap igsp-admin-wrap">
<div class="igsp-admin-header">
<h1>
<span class="dashicons dashicons-instagram"></span>
<?php esc_html_e('Instagram Gallery Sync Pro', 'instagram-gallery-sync-pro'); ?>
</h1>
<p class="igsp-version">
<?php echo esc_html(sprintf(__('Version %s', 'instagram-gallery-sync-pro'), IGSP_VERSION)); ?>
</p>
</div>
<div class="igsp-admin-content">
<!-- Tabs Navigation -->
<nav class="igsp-tabs-nav">
<?php foreach ($tabs as $tab_id => $tab): ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=' . IGSP_Admin::PAGE_SLUG . '&tab=' . $tab_id)); ?>"
class="igsp-tab-link <?php echo $current_tab === $tab_id ? 'active' : ''; ?>">
<span class="dashicons <?php echo esc_attr($tab['icon']); ?>"></span>
<?php echo esc_html($tab['title']); ?>
</a>
<?php endforeach; ?>
</nav>
<!-- Tab Content -->
<div class="igsp-tab-content">
<?php
$tab_file = IGSP_PLUGIN_DIR . 'admin/views/tab-' . $current_tab . '.php';
if (file_exists($tab_file)) {
include $tab_file;
} else {
echo '<p>' . esc_html__('Tab content not found.', 'instagram-gallery-sync-pro') . '</p>';
}
?>
</div>
</div>
<!-- Sidebar -->
<div class="igsp-admin-sidebar">
<div class="igsp-sidebar-box igsp-status-box">
<h3>
<?php esc_html_e('Sync Status', 'instagram-gallery-sync-pro'); ?>
</h3>
<div class="igsp-status-info" id="igsp-status-info">
<?php
$cron = new IGSP_Cron();
$status = $cron->get_sync_status();
?>
<div class="igsp-status-row">
<span class="igsp-status-label">
<?php esc_html_e('Total Posts:', 'instagram-gallery-sync-pro'); ?>
</span>
<span class="igsp-status-value" id="igsp-total-posts">
<?php echo esc_html($status['total_posts']); ?>
</span>
</div>
<div class="igsp-status-row">
<span class="igsp-status-label">
<?php esc_html_e('Last Sync:', 'instagram-gallery-sync-pro'); ?>
</span>
<span class="igsp-status-value" id="igsp-last-sync">
<?php echo esc_html($status['last_sync']); ?>
</span>
</div>
<div class="igsp-status-row">
<span class="igsp-status-label">
<?php esc_html_e('Next Sync:', 'instagram-gallery-sync-pro'); ?>
</span>
<span class="igsp-status-value" id="igsp-next-sync">
<?php echo esc_html($status['next_sync']); ?>
</span>
</div>
</div>
</div>
<div class="igsp-sidebar-box igsp-shortcode-box">
<h3>
<?php esc_html_e('Shortcode', 'instagram-gallery-sync-pro'); ?>
</h3>
<code class="igsp-shortcode-display">[instagram_gallery]</code>
<p class="description">
<?php esc_html_e('Use this shortcode to display the gallery on any page or post.', 'instagram-gallery-sync-pro'); ?>
</p>
</div>
<div class="igsp-sidebar-box igsp-help-box">
<h3>
<?php esc_html_e('Need Help?', 'instagram-gallery-sync-pro'); ?>
</h3>
<ul>
<li>
<span class="dashicons dashicons-book"></span>
<a href="#" target="_blank">
<?php esc_html_e('Documentation', 'instagram-gallery-sync-pro'); ?>
</a>
</li>
<li>
<span class="dashicons dashicons-sos"></span>
<a href="#" target="_blank">
<?php esc_html_e('Support', 'instagram-gallery-sync-pro'); ?>
</a>
</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,264 @@
<?php
/**
* Advanced Settings Tab
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$logger = new IGSP_Logger();
$logs = $logger->get_formatted_logs(50);
$image_handler = new IGSP_Image_Handler();
?>
<form method="post" action="options.php" class="igsp-settings-form" id="igsp-advanced-form">
<?php settings_fields('igsp_advanced_settings'); ?>
<div class="igsp-section">
<h2>
<?php esc_html_e('Debug & Development', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<?php esc_html_e('Debug Mode', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<?php $debug = get_option('igsp_debug_mode', 'no'); ?>
<fieldset>
<label>
<input type="radio" name="igsp_debug_mode" value="yes" <?php checked($debug, 'yes'); ?>>
<?php esc_html_e('Enabled', 'instagram-gallery-sync-pro'); ?>
</label>
<label>
<input type="radio" name="igsp_debug_mode" value="no" <?php checked($debug, 'no'); ?>>
<?php esc_html_e('Disabled', 'instagram-gallery-sync-pro'); ?>
</label>
</fieldset>
<p class="description">
<?php esc_html_e('Enable to log all sync activities. Disable in production for better performance.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Performance', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_cache_duration">
<?php esc_html_e('Cache Duration', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $cache = get_option('igsp_cache_duration', 3600); ?>
<select id="igsp_cache_duration" name="igsp_cache_duration">
<option value="3600" <?php selected($cache, 3600); ?>>
<?php esc_html_e('1 Hour', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="21600" <?php selected($cache, 21600); ?>>
<?php esc_html_e('6 Hours', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="43200" <?php selected($cache, 43200); ?>>
<?php esc_html_e('12 Hours', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="86400" <?php selected($cache, 86400); ?>>
<?php esc_html_e('24 Hours', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
<p class="description">
<?php esc_html_e('How long to cache the gallery HTML.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Request Timeout', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_request_timeout" name="igsp_request_timeout" min="10" max="60"
value="<?php echo esc_attr(get_option('igsp_request_timeout', 30)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_request_timeout', 30)); ?>s
</span>
</div>
<p class="description">
<?php esc_html_e('Timeout for Instagram requests in seconds.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Auto-Delete Old Posts', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_auto_delete_days" name="igsp_auto_delete_days" min="0" max="365"
step="30" value="<?php echo esc_attr(get_option('igsp_auto_delete_days', 0)); ?>">
<span class="igsp-slider-value">
<?php
$days = get_option('igsp_auto_delete_days', 0);
echo $days === 0 ? esc_html__('Never', 'instagram-gallery-sync-pro') : esc_html($days . ' ' . __('days', 'instagram-gallery-sync-pro'));
?>
</span>
</div>
<p class="description">
<?php esc_html_e('Automatically delete posts older than X days. Set to 0 to disable.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Request Configuration', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_user_agent">
<?php esc_html_e('Custom User-Agent', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<input type="text" id="igsp_user_agent" name="igsp_user_agent"
value="<?php echo esc_attr(get_option('igsp_user_agent', '')); ?>" class="large-text"
placeholder="<?php esc_attr_e('Leave empty to use rotating User-Agents', 'instagram-gallery-sync-pro'); ?>">
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Proxy Settings', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-inline-fields">
<input type="text" id="igsp_proxy_host" name="igsp_proxy_host"
value="<?php echo esc_attr(get_option('igsp_proxy_host', '')); ?>"
placeholder="<?php esc_attr_e('Proxy Host', 'instagram-gallery-sync-pro'); ?>"
class="regular-text">
<input type="number" id="igsp_proxy_port" name="igsp_proxy_port"
value="<?php echo esc_attr(get_option('igsp_proxy_port', '')); ?>"
placeholder="<?php esc_attr_e('Port', 'instagram-gallery-sync-pro'); ?>"
class="small-text" min="1" max="65535">
</div>
<p class="description">
<?php esc_html_e('Optional: Use a proxy for Instagram requests.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
</table>
</div>
<?php submit_button(); ?>
</form>
<div class="igsp-section igsp-actions-section">
<h2>
<?php esc_html_e('Tools', 'instagram-gallery-sync-pro'); ?>
</h2>
<div class="igsp-tools-grid">
<div class="igsp-tool-card">
<h3>
<?php esc_html_e('Clear Cache', 'instagram-gallery-sync-pro'); ?>
</h3>
<p>
<?php esc_html_e('Clear all cached gallery data.', 'instagram-gallery-sync-pro'); ?>
</p>
<button type="button" id="igsp-clear-cache" class="button">
<span class="dashicons dashicons-trash"></span>
<?php esc_html_e('Clear Cache', 'instagram-gallery-sync-pro'); ?>
</button>
</div>
<div class="igsp-tool-card">
<h3>
<?php esc_html_e('Storage Used', 'instagram-gallery-sync-pro'); ?>
</h3>
<p>
<?php echo esc_html(sprintf(__('Total: %s', 'instagram-gallery-sync-pro'), $image_handler->get_storage_used_formatted())); ?>
</p>
</div>
<div class="igsp-tool-card igsp-tool-danger">
<h3>
<?php esc_html_e('Reset All Data', 'instagram-gallery-sync-pro'); ?>
</h3>
<p>
<?php esc_html_e('Delete ALL Instagram data, images, and logs.', 'instagram-gallery-sync-pro'); ?>
</p>
<button type="button" id="igsp-reset-data" class="button button-danger">
<span class="dashicons dashicons-warning"></span>
<?php esc_html_e('Reset Everything', 'instagram-gallery-sync-pro'); ?>
</button>
</div>
</div>
</div>
<div class="igsp-section igsp-logs-section">
<div class="igsp-logs-header">
<h2>
<?php esc_html_e('Activity Logs', 'instagram-gallery-sync-pro'); ?>
</h2>
<button type="button" id="igsp-clear-logs" class="button button-small">
<?php esc_html_e('Clear Logs', 'instagram-gallery-sync-pro'); ?>
</button>
</div>
<?php if (empty($logs)): ?>
<p class="igsp-no-logs">
<?php esc_html_e('No log entries yet. Enable debug mode to see activity logs.', 'instagram-gallery-sync-pro'); ?>
</p>
<?php else: ?>
<div class="igsp-logs-table-wrapper">
<table class="igsp-logs-table widefat">
<thead>
<tr>
<th class="igsp-log-type">
<?php esc_html_e('Type', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-log-message">
<?php esc_html_e('Message', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-log-date">
<?php esc_html_e('Date', 'instagram-gallery-sync-pro'); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr class="<?php echo esc_attr($log['type_class']); ?>">
<td>
<span class="igsp-log-badge igsp-log-<?php echo esc_attr($log['type']); ?>">
<?php echo esc_html($log['type_label']); ?>
</span>
</td>
<td>
<?php echo esc_html($log['message']); ?>
</td>
<td title="<?php echo esc_attr($log['date']); ?>">
<?php echo esc_html($log['relative']); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>

233
admin/views/tab-display.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
/**
* Display Settings Tab
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<form method="post" action="options.php" class="igsp-settings-form">
<?php settings_fields('igsp_display_settings'); ?>
<div class="igsp-section">
<h2>
<?php esc_html_e('Display Options', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<?php esc_html_e('Number of Images to Display', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_display_limit" name="igsp_display_limit" min="1" max="50"
value="<?php echo esc_attr(get_option('igsp_display_limit', 12)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_display_limit', 12)); ?>
</span>
</div>
<p class="description">
<?php esc_html_e('Number of images to show in the gallery. Set to 50 for "All".', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_order">
<?php esc_html_e('Display Order', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $order = get_option('igsp_order', 'newest'); ?>
<select id="igsp_order" name="igsp_order">
<option value="newest" <?php selected($order, 'newest'); ?>>
<?php esc_html_e('Newest First', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="oldest" <?php selected($order, 'oldest'); ?>>
<?php esc_html_e('Oldest First', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="random" <?php selected($order, 'random'); ?>>
<?php esc_html_e('Random', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Show Captions', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<?php $show_caption = get_option('igsp_show_caption', 'no'); ?>
<fieldset>
<label>
<input type="radio" name="igsp_show_caption" value="yes" <?php checked($show_caption, 'yes'); ?>>
<?php esc_html_e('Yes', 'instagram-gallery-sync-pro'); ?>
</label>
<label>
<input type="radio" name="igsp_show_caption" value="no" <?php checked($show_caption, 'no'); ?>>
<?php esc_html_e('No', 'instagram-gallery-sync-pro'); ?>
</label>
</fieldset>
</td>
</tr>
<tr class="igsp-conditional" data-depends-on="igsp_show_caption" data-depends-value="yes">
<th scope="row">
<label for="igsp_caption_position">
<?php esc_html_e('Caption Position', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $position = get_option('igsp_caption_position', 'overlay'); ?>
<select id="igsp_caption_position" name="igsp_caption_position">
<option value="overlay" <?php selected($position, 'overlay'); ?>>
<?php esc_html_e('Overlay on Image', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="below" <?php selected($position, 'below'); ?>>
<?php esc_html_e('Below Image', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Show "More on Instagram" Button', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<?php $show_btn = get_option('igsp_show_instagram_btn', 'yes'); ?>
<fieldset>
<label>
<input type="radio" name="igsp_show_instagram_btn" value="yes" <?php checked($show_btn, 'yes'); ?>>
<?php esc_html_e('Yes', 'instagram-gallery-sync-pro'); ?>
</label>
<label>
<input type="radio" name="igsp_show_instagram_btn" value="no" <?php checked($show_btn, 'no'); ?>>
<?php esc_html_e('No', 'instagram-gallery-sync-pro'); ?>
</label>
</fieldset>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Interactivity', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_link_behavior">
<?php esc_html_e('Link Behavior', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $link = get_option('igsp_link_behavior', 'new_tab'); ?>
<select id="igsp_link_behavior" name="igsp_link_behavior">
<option value="new_tab" <?php selected($link, 'new_tab'); ?>>
<?php esc_html_e('Open in New Tab', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="same_window" <?php selected($link, 'same_window'); ?>>
<?php esc_html_e('Open in Same Window', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="lightbox" <?php selected($link, 'lightbox'); ?>>
<?php esc_html_e('Open in Lightbox', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="none" <?php selected($link, 'none'); ?>>
<?php esc_html_e('No Link', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Enable Lightbox', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<?php $lightbox = get_option('igsp_lightbox', 'yes'); ?>
<fieldset>
<label>
<input type="radio" name="igsp_lightbox" value="yes" <?php checked($lightbox, 'yes'); ?>>
<?php esc_html_e('Yes', 'instagram-gallery-sync-pro'); ?>
</label>
<label>
<input type="radio" name="igsp_lightbox" value="no" <?php checked($lightbox, 'no'); ?>>
<?php esc_html_e('No', 'instagram-gallery-sync-pro'); ?>
</label>
</fieldset>
<p class="description">
<?php esc_html_e('Allow clicking images to view them in a lightbox.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Performance', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<?php esc_html_e('Lazy Loading', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<?php $lazy = get_option('igsp_lazy_loading', 'yes'); ?>
<fieldset>
<label>
<input type="radio" name="igsp_lazy_loading" value="yes" <?php checked($lazy, 'yes'); ?>>
<?php esc_html_e('Yes (Recommended)', 'instagram-gallery-sync-pro'); ?>
</label>
<label>
<input type="radio" name="igsp_lazy_loading" value="no" <?php checked($lazy, 'no'); ?>>
<?php esc_html_e('No', 'instagram-gallery-sync-pro'); ?>
</label>
</fieldset>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_loader_type">
<?php esc_html_e('Loading Animation', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $loader = get_option('igsp_loader_type', 'spinner'); ?>
<select id="igsp_loader_type" name="igsp_loader_type">
<option value="spinner" <?php selected($loader, 'spinner'); ?>>
<?php esc_html_e('Spinner', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="dots" <?php selected($loader, 'dots'); ?>>
<?php esc_html_e('Dots', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="pulse" <?php selected($loader, 'pulse'); ?>>
<?php esc_html_e('Pulse', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="skeleton" <?php selected($loader, 'skeleton'); ?>>
<?php esc_html_e('Skeleton', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="none" <?php selected($loader, 'none'); ?>>
<?php esc_html_e('None', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
</table>
</div>
<?php submit_button(); ?>
</form>

View File

@@ -0,0 +1,153 @@
<?php
/**
* Instagram Settings Tab
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$cron = new IGSP_Cron();
$intervals = $cron->get_intervals();
?>
<form method="post" action="options.php" class="igsp-settings-form">
<?php settings_fields( 'igsp_instagram_settings' ); ?>
<div class="igsp-section">
<h2><?php esc_html_e( 'Account Settings', 'instagram-gallery-sync-pro' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_username"><?php esc_html_e( 'Instagram Username', 'instagram-gallery-sync-pro' ); ?></label>
</th>
<td>
<div class="igsp-input-wrapper">
<span class="igsp-input-prefix">@</span>
<input type="text"
id="igsp_username"
name="igsp_username"
value="<?php echo esc_attr( get_option( 'igsp_username', '' ) ); ?>"
class="regular-text"
placeholder="username">
</div>
<p class="description"><?php esc_html_e( 'Enter the Instagram username without the @ symbol. The profile must be public.', 'instagram-gallery-sync-pro' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_max_images"><?php esc_html_e( 'Maximum Images to Fetch', 'instagram-gallery-sync-pro' ); ?></label>
</th>
<td>
<select id="igsp_max_images" name="igsp_max_images">
<?php
$max_images = get_option( 'igsp_max_images', 12 );
$options = array( 12, 24, 36, 48, 100 );
foreach ( $options as $option ) :
?>
<option value="<?php echo esc_attr( $option ); ?>" <?php selected( $max_images, $option ); ?>>
<?php echo esc_html( $option === 100 ? __( 'All available', 'instagram-gallery-sync-pro' ) : $option ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Number of images to fetch from Instagram.', 'instagram-gallery-sync-pro' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Save Images Locally', 'instagram-gallery-sync-pro' ); ?></th>
<td>
<fieldset>
<label>
<input type="radio"
name="igsp_save_locally"
value="yes"
<?php checked( get_option( 'igsp_save_locally', 'yes' ), 'yes' ); ?>>
<?php esc_html_e( 'Yes - Download and store images on your server (recommended)', 'instagram-gallery-sync-pro' ); ?>
</label>
<br>
<label>
<input type="radio"
name="igsp_save_locally"
value="no"
<?php checked( get_option( 'igsp_save_locally', 'yes' ), 'no' ); ?>>
<?php esc_html_e( 'No - Link directly to Instagram (may cause loading issues)', 'instagram-gallery-sync-pro' ); ?>
</label>
</fieldset>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_image_quality"><?php esc_html_e( 'Image Quality', 'instagram-gallery-sync-pro' ); ?></label>
</th>
<td>
<select id="igsp_image_quality" name="igsp_image_quality">
<?php
$quality = get_option( 'igsp_image_quality', 'high' );
$quality_options = array(
'thumbnail' => __( 'Thumbnail (150px)', 'instagram-gallery-sync-pro' ),
'medium' => __( 'Medium (320px)', 'instagram-gallery-sync-pro' ),
'high' => __( 'High (640px+)', 'instagram-gallery-sync-pro' ),
);
foreach ( $quality_options as $value => $label ) :
?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $quality, $value ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2><?php esc_html_e( 'Synchronization', 'instagram-gallery-sync-pro' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_sync_interval"><?php esc_html_e( 'Sync Interval', 'instagram-gallery-sync-pro' ); ?></label>
</th>
<td>
<select id="igsp_sync_interval" name="igsp_sync_interval">
<?php
$current_interval = get_option( 'igsp_sync_interval', 'daily' );
foreach ( $intervals as $value => $label ) :
?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $current_interval, $value ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'How often to check for new posts.', 'instagram-gallery-sync-pro' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Manual Sync', 'instagram-gallery-sync-pro' ); ?></th>
<td>
<button type="button" id="igsp-sync-now" class="button button-primary">
<span class="dashicons dashicons-update"></span>
<?php esc_html_e( 'Sync Now', 'instagram-gallery-sync-pro' ); ?>
</button>
<div id="igsp-sync-progress" class="igsp-progress-wrapper" style="display: none;">
<div class="igsp-progress-bar">
<div class="igsp-progress-fill"></div>
</div>
<span class="igsp-progress-text"><?php esc_html_e( 'Syncing...', 'instagram-gallery-sync-pro' ); ?></span>
</div>
<div id="igsp-sync-result" class="igsp-sync-result" style="display: none;"></div>
</td>
</tr>
</table>
</div>
<?php submit_button(); ?>
</form>

279
admin/views/tab-layout.php Normal file
View File

@@ -0,0 +1,279 @@
<?php
/**
* Layout Settings Tab
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<form method="post" action="options.php" class="igsp-settings-form">
<?php settings_fields('igsp_layout_settings'); ?>
<div class="igsp-section">
<h2>
<?php esc_html_e('Layout Type', 'instagram-gallery-sync-pro'); ?>
</h2>
<?php $layout_type = get_option('igsp_layout_type', 'grid'); ?>
<div class="igsp-layout-selector">
<label class="igsp-layout-option <?php echo $layout_type === 'grid' ? 'selected' : ''; ?>">
<input type="radio" name="igsp_layout_type" value="grid" <?php checked($layout_type, 'grid'); ?>>
<div class="igsp-layout-preview igsp-preview-grid">
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</div>
<span class="igsp-layout-name">
<?php esc_html_e('Grid', 'instagram-gallery-sync-pro'); ?>
</span>
</label>
<label class="igsp-layout-option <?php echo $layout_type === 'masonry' ? 'selected' : ''; ?>">
<input type="radio" name="igsp_layout_type" value="masonry" <?php checked($layout_type, 'masonry'); ?>>
<div class="igsp-layout-preview igsp-preview-masonry">
<span style="height: 40px;"></span><span style="height: 60px;"></span><span
style="height: 30px;"></span>
<span style="height: 50px;"></span><span style="height: 35px;"></span><span
style="height: 55px;"></span>
</div>
<span class="igsp-layout-name">
<?php esc_html_e('Masonry', 'instagram-gallery-sync-pro'); ?>
</span>
</label>
<label class="igsp-layout-option <?php echo $layout_type === 'justified' ? 'selected' : ''; ?>">
<input type="radio" name="igsp_layout_type" value="justified" <?php checked($layout_type, 'justified'); ?>>
<div class="igsp-layout-preview igsp-preview-justified">
<span style="width: 60px;"></span><span style="width: 40px;"></span>
<span style="width: 35px;"></span><span style="width: 65px;"></span>
</div>
<span class="igsp-layout-name">
<?php esc_html_e('Justified', 'instagram-gallery-sync-pro'); ?>
</span>
</label>
<label class="igsp-layout-option <?php echo $layout_type === 'slider' ? 'selected' : ''; ?>">
<input type="radio" name="igsp_layout_type" value="slider" <?php checked($layout_type, 'slider'); ?>>
<div class="igsp-layout-preview igsp-preview-slider">
<span class="arrow-left"></span>
<span class="slide"></span>
<span class="arrow-right"></span>
</div>
<span class="igsp-layout-name">
<?php esc_html_e('Slider', 'instagram-gallery-sync-pro'); ?>
</span>
</label>
<label class="igsp-layout-option <?php echo $layout_type === 'list' ? 'selected' : ''; ?>">
<input type="radio" name="igsp_layout_type" value="list" <?php checked($layout_type, 'list'); ?>>
<div class="igsp-layout-preview igsp-preview-list">
<span></span>
<span></span>
<span></span>
</div>
<span class="igsp-layout-name">
<?php esc_html_e('List', 'instagram-gallery-sync-pro'); ?>
</span>
</label>
</div>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Grid Settings', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<?php esc_html_e('Columns (Desktop)', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_columns_desktop" name="igsp_columns_desktop" min="1" max="6"
value="<?php echo esc_attr(get_option('igsp_columns_desktop', 3)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_columns_desktop', 3)); ?>
</span>
</div>
<p class="description">
<?php esc_html_e('Number of columns on desktop (> 1024px)', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Columns (Tablet)', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_columns_tablet" name="igsp_columns_tablet" min="1" max="4"
value="<?php echo esc_attr(get_option('igsp_columns_tablet', 2)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_columns_tablet', 2)); ?>
</span>
</div>
<p class="description">
<?php esc_html_e('Number of columns on tablet (768px - 1023px)', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Columns (Mobile)', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_columns_mobile" name="igsp_columns_mobile" min="1" max="2"
value="<?php echo esc_attr(get_option('igsp_columns_mobile', 1)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_columns_mobile', 1)); ?>
</span>
</div>
<p class="description">
<?php esc_html_e('Number of columns on mobile (< 767px)', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Spacing', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_spacing" name="igsp_spacing" min="0" max="50"
value="<?php echo esc_attr(get_option('igsp_spacing', 10)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_spacing', 10)); ?>px
</span>
</div>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Outer Padding', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_padding" name="igsp_padding" min="0" max="50"
value="<?php echo esc_attr(get_option('igsp_padding', 0)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_padding', 0)); ?>px
</span>
</div>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Image Settings', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_aspect_ratio">
<?php esc_html_e('Aspect Ratio', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $aspect = get_option('igsp_aspect_ratio', 'square'); ?>
<select id="igsp_aspect_ratio" name="igsp_aspect_ratio">
<option value="original" <?php selected($aspect, 'original'); ?>>
<?php esc_html_e('Original', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="square" <?php selected($aspect, 'square'); ?>>
<?php esc_html_e('Square (1:1)', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="4-3" <?php selected($aspect, '4-3'); ?>>
<?php esc_html_e('4:3', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="16-9" <?php selected($aspect, '16-9'); ?>>
<?php esc_html_e('16:9', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="3-4" <?php selected($aspect, '3-4'); ?>>
<?php esc_html_e('3:4 (Portrait)', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_object_fit">
<?php esc_html_e('Object Fit', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $fit = get_option('igsp_object_fit', 'cover'); ?>
<select id="igsp_object_fit" name="igsp_object_fit">
<option value="cover" <?php selected($fit, 'cover'); ?>>
<?php esc_html_e('Cover (Crop to fill)', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="contain" <?php selected($fit, 'contain'); ?>>
<?php esc_html_e('Contain (Fit inside)', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e('Border Radius', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_border_radius" name="igsp_border_radius" min="0" max="30"
value="<?php echo esc_attr(get_option('igsp_border_radius', 0)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_border_radius', 0)); ?>px
</span>
</div>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_hover_effect">
<?php esc_html_e('Hover Effect', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<?php $hover = get_option('igsp_hover_effect', 'zoom'); ?>
<select id="igsp_hover_effect" name="igsp_hover_effect">
<option value="none" <?php selected($hover, 'none'); ?>>
<?php esc_html_e('None', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="zoom" <?php selected($hover, 'zoom'); ?>>
<?php esc_html_e('Zoom', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="fade" <?php selected($hover, 'fade'); ?>>
<?php esc_html_e('Fade + Caption', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="overlay" <?php selected($hover, 'overlay'); ?>>
<?php esc_html_e('Overlay with Icon', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="grayscale" <?php selected($hover, 'grayscale'); ?>>
<?php esc_html_e('Grayscale to Color', 'instagram-gallery-sync-pro'); ?>
</option>
<option value="lift" <?php selected($hover, 'lift'); ?>>
<?php esc_html_e('Lift (Shadow + Transform)', 'instagram-gallery-sync-pro'); ?>
</option>
</select>
</td>
</tr>
</table>
</div>
<?php submit_button(); ?>
</form>

158
admin/views/tab-styling.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
/**
* Styling Settings Tab
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<form method="post" action="options.php" class="igsp-settings-form">
<?php settings_fields('igsp_styling_settings'); ?>
<div class="igsp-section">
<h2>
<?php esc_html_e('Colors', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_primary_color">
<?php esc_html_e('Primary Color', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<input type="text" id="igsp_primary_color" name="igsp_primary_color"
value="<?php echo esc_attr(get_option('igsp_primary_color', '#e1306c')); ?>"
class="igsp-color-picker" data-default-color="#e1306c">
<p class="description">
<?php esc_html_e('Used for buttons and highlights.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_hover_color">
<?php esc_html_e('Hover Color', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<input type="text" id="igsp_hover_color" name="igsp_hover_color"
value="<?php echo esc_attr(get_option('igsp_hover_color', '#c13584')); ?>"
class="igsp-color-picker" data-default-color="#c13584">
<p class="description">
<?php esc_html_e('Used for overlay and hover effects.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_text_color">
<?php esc_html_e('Text Color', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<input type="text" id="igsp_text_color" name="igsp_text_color"
value="<?php echo esc_attr(get_option('igsp_text_color', '#ffffff')); ?>"
class="igsp-color-picker" data-default-color="#ffffff">
<p class="description">
<?php esc_html_e('Used for caption and overlay text.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Typography', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<?php esc_html_e('Caption Font Size', 'instagram-gallery-sync-pro'); ?>
</th>
<td>
<div class="igsp-slider-wrapper">
<input type="range" id="igsp_caption_font_size" name="igsp_caption_font_size" min="10" max="24"
value="<?php echo esc_attr(get_option('igsp_caption_font_size', 14)); ?>">
<span class="igsp-slider-value">
<?php echo esc_html(get_option('igsp_caption_font_size', 14)); ?>px
</span>
</div>
</td>
</tr>
</table>
</div>
<div class="igsp-section">
<h2>
<?php esc_html_e('Advanced Styling', 'instagram-gallery-sync-pro'); ?>
</h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="igsp_css_prefix">
<?php esc_html_e('CSS Class Prefix', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<input type="text" id="igsp_css_prefix" name="igsp_css_prefix"
value="<?php echo esc_attr(get_option('igsp_css_prefix', 'igsp')); ?>" class="regular-text">
<p class="description">
<?php esc_html_e('Prefix for all CSS classes. Change only if you have conflicts with your theme.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="igsp_custom_css">
<?php esc_html_e('Custom CSS', 'instagram-gallery-sync-pro'); ?>
</label>
</th>
<td>
<textarea id="igsp_custom_css" name="igsp_custom_css" rows="10" class="large-text code"
placeholder="/* Add your custom CSS here */"><?php echo esc_textarea(get_option('igsp_custom_css', '')); ?></textarea>
<p class="description">
<?php esc_html_e('Add custom CSS to override default styles. Use cautiously.', 'instagram-gallery-sync-pro'); ?>
</p>
</td>
</tr>
</table>
</div>
<div class="igsp-section igsp-preview-section">
<h2>
<?php esc_html_e('Live Preview', 'instagram-gallery-sync-pro'); ?>
</h2>
<div class="igsp-style-preview">
<div class="igsp-preview-item" style="--primary: <?php echo esc_attr(get_option('igsp_primary_color', '#e1306c')); ?>;
--hover: <?php echo esc_attr(get_option('igsp_hover_color', '#c13584')); ?>;
--text: <?php echo esc_attr(get_option('igsp_text_color', '#ffffff')); ?>;">
<div class="igsp-preview-image">
<div class="igsp-preview-overlay">
<span class="igsp-preview-caption"
style="font-size: <?php echo esc_attr(get_option('igsp_caption_font_size', 14)); ?>px;">
<?php esc_html_e('Sample Caption', 'instagram-gallery-sync-pro'); ?>
</span>
</div>
</div>
<button class="igsp-preview-button">
<?php esc_html_e('Sample Button', 'instagram-gallery-sync-pro'); ?>
</button>
</div>
</div>
</div>
<?php submit_button(); ?>
</form>

395
includes/class-admin.php Normal file
View File

@@ -0,0 +1,395 @@
<?php
/**
* Admin Class
*
* Handles all admin functionality including settings pages and AJAX handlers.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Admin
*/
class IGSP_Admin
{
/**
* Settings page slug
*
* @var string
*/
const PAGE_SLUG = 'instagram-gallery-sync-pro';
/**
* Initialize admin
*
* @return void
*/
public function init()
{
// Add admin menu
add_action('admin_menu', array($this, 'add_admin_menu'));
// Register settings
add_action('admin_init', array($this, 'register_settings'));
// Enqueue admin assets
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
// AJAX handlers
add_action('wp_ajax_igsp_manual_sync', array($this, 'ajax_manual_sync'));
add_action('wp_ajax_igsp_clear_cache', array($this, 'ajax_clear_cache'));
add_action('wp_ajax_igsp_reset_data', array($this, 'ajax_reset_data'));
add_action('wp_ajax_igsp_get_sync_status', array($this, 'ajax_get_sync_status'));
add_action('wp_ajax_igsp_clear_logs', array($this, 'ajax_clear_logs'));
// Add settings link to plugins page
add_filter('plugin_action_links_' . IGSP_PLUGIN_BASENAME, array($this, 'add_settings_link'));
}
/**
* Add admin menu
*
* @return void
*/
public function add_admin_menu()
{
add_menu_page(
__('Instagram Gallery', 'instagram-gallery-sync-pro'),
__('Instagram Gallery', 'instagram-gallery-sync-pro'),
'manage_options',
self::PAGE_SLUG,
array($this, 'render_settings_page'),
'dashicons-instagram',
30
);
}
/**
* Register settings
*
* @return void
*/
public function register_settings()
{
// Instagram settings
register_setting('igsp_instagram_settings', 'igsp_username', 'sanitize_text_field');
register_setting('igsp_instagram_settings', 'igsp_max_images', 'absint');
register_setting('igsp_instagram_settings', 'igsp_save_locally', 'sanitize_text_field');
register_setting('igsp_instagram_settings', 'igsp_image_quality', 'sanitize_text_field');
register_setting('igsp_instagram_settings', 'igsp_sync_interval', 'sanitize_text_field');
// Layout settings
register_setting('igsp_layout_settings', 'igsp_layout_type', 'sanitize_text_field');
register_setting('igsp_layout_settings', 'igsp_columns_desktop', 'absint');
register_setting('igsp_layout_settings', 'igsp_columns_tablet', 'absint');
register_setting('igsp_layout_settings', 'igsp_columns_mobile', 'absint');
register_setting('igsp_layout_settings', 'igsp_spacing', 'absint');
register_setting('igsp_layout_settings', 'igsp_padding', 'absint');
register_setting('igsp_layout_settings', 'igsp_aspect_ratio', 'sanitize_text_field');
register_setting('igsp_layout_settings', 'igsp_object_fit', 'sanitize_text_field');
register_setting('igsp_layout_settings', 'igsp_border_radius', 'absint');
register_setting('igsp_layout_settings', 'igsp_hover_effect', 'sanitize_text_field');
// Display settings
register_setting('igsp_display_settings', 'igsp_display_limit', 'absint');
register_setting('igsp_display_settings', 'igsp_order', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_show_caption', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_caption_position', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_show_instagram_btn', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_link_behavior', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_lightbox', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_lazy_loading', 'sanitize_text_field');
register_setting('igsp_display_settings', 'igsp_loader_type', 'sanitize_text_field');
// Styling settings
register_setting('igsp_styling_settings', 'igsp_primary_color', 'sanitize_hex_color');
register_setting('igsp_styling_settings', 'igsp_hover_color', 'sanitize_hex_color');
register_setting('igsp_styling_settings', 'igsp_text_color', 'sanitize_hex_color');
register_setting('igsp_styling_settings', 'igsp_caption_font_size', 'absint');
register_setting('igsp_styling_settings', 'igsp_custom_css', array($this, 'sanitize_css'));
register_setting('igsp_styling_settings', 'igsp_css_prefix', 'sanitize_text_field');
// Advanced settings
register_setting('igsp_advanced_settings', 'igsp_debug_mode', 'sanitize_text_field');
register_setting('igsp_advanced_settings', 'igsp_cache_duration', 'absint');
register_setting('igsp_advanced_settings', 'igsp_request_timeout', 'absint');
register_setting('igsp_advanced_settings', 'igsp_user_agent', 'sanitize_text_field');
register_setting('igsp_advanced_settings', 'igsp_proxy_host', 'sanitize_text_field');
register_setting('igsp_advanced_settings', 'igsp_proxy_port', 'absint');
register_setting('igsp_advanced_settings', 'igsp_auto_delete_days', 'absint');
}
/**
* Sanitize CSS input
*
* @param string $css CSS string
* @return string
*/
public function sanitize_css($css)
{
// Remove any script tags
$css = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $css);
// Remove javascript: URLs
$css = preg_replace('/javascript\s*:/i', '', $css);
// Remove expression()
$css = preg_replace('/expression\s*\(/i', '', $css);
// Remove behavior: (IE specific)
$css = preg_replace('/behavior\s*:/i', '', $css);
return wp_strip_all_tags($css);
}
/**
* Enqueue admin assets
*
* @param string $hook Current page hook
* @return void
*/
public function enqueue_assets($hook)
{
if (strpos($hook, self::PAGE_SLUG) === false) {
return;
}
// Admin CSS
wp_enqueue_style(
'igsp-admin-style',
IGSP_PLUGIN_URL . 'admin/css/admin-style.css',
array(),
IGSP_VERSION
);
// WordPress components
wp_enqueue_style('wp-color-picker');
// Admin JS
wp_enqueue_script(
'igsp-admin-script',
IGSP_PLUGIN_URL . 'admin/js/admin-script.js',
array('jquery', 'wp-color-picker'),
IGSP_VERSION,
true
);
// AJAX Sync JS
wp_enqueue_script(
'igsp-ajax-sync',
IGSP_PLUGIN_URL . 'admin/js/ajax-sync.js',
array('jquery'),
IGSP_VERSION,
true
);
// Localize script
wp_localize_script('igsp-admin-script', 'igspAdmin', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('igsp_admin_nonce'),
'strings' => array(
'syncing' => __('Syncing...', 'instagram-gallery-sync-pro'),
'syncComplete' => __('Sync complete!', 'instagram-gallery-sync-pro'),
'syncError' => __('Sync failed. Check logs for details.', 'instagram-gallery-sync-pro'),
'confirmReset' => __('Are you sure? This will delete ALL Instagram data including images. This action cannot be undone.', 'instagram-gallery-sync-pro'),
'confirmClearLogs' => __('Are you sure you want to clear all logs?', 'instagram-gallery-sync-pro'),
'processing' => __('Processing...', 'instagram-gallery-sync-pro'),
'done' => __('Done!', 'instagram-gallery-sync-pro'),
),
));
}
/**
* Render settings page
*
* @return void
*/
public function render_settings_page()
{
// Check permissions
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'instagram-gallery-sync-pro'));
}
include IGSP_PLUGIN_DIR . 'admin/views/settings-page.php';
}
/**
* Add settings link to plugins page
*
* @param array $links Existing links
* @return array
*/
public function add_settings_link($links)
{
$settings_link = sprintf(
'<a href="%s">%s</a>',
admin_url('admin.php?page=' . self::PAGE_SLUG),
__('Settings', 'instagram-gallery-sync-pro')
);
array_unshift($links, $settings_link);
return $links;
}
/**
* AJAX: Manual sync
*
* @return void
*/
public function ajax_manual_sync()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
$cron = new IGSP_Cron();
$result = $cron->manual_sync();
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result);
}
}
/**
* AJAX: Clear cache
*
* @return void
*/
public function ajax_clear_cache()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
igsp()->clear_transients();
wp_send_json_success(array('message' => __('Cache cleared successfully.', 'instagram-gallery-sync-pro')));
}
/**
* AJAX: Reset all data
*
* @return void
*/
public function ajax_reset_data()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
// Delete all posts from database
$database = new IGSP_Database();
$database->delete_all_posts();
// Clear all images
$image_handler = new IGSP_Image_Handler();
$image_handler->clear_all_images();
// Clear logs
$logger = new IGSP_Logger();
$logger->clear_logs();
// Clear transients
igsp()->clear_transients();
// Reset last sync
update_option('igsp_last_sync', '');
wp_send_json_success(array('message' => __('All data has been reset.', 'instagram-gallery-sync-pro')));
}
/**
* AJAX: Get sync status
*
* @return void
*/
public function ajax_get_sync_status()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
$cron = new IGSP_Cron();
$status = $cron->get_sync_status();
wp_send_json_success($status);
}
/**
* AJAX: Clear logs
*
* @return void
*/
public function ajax_clear_logs()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
$logger = new IGSP_Logger();
$logger->clear_logs();
wp_send_json_success(array('message' => __('Logs cleared successfully.', 'instagram-gallery-sync-pro')));
}
/**
* Get current tab
*
* @return string
*/
public function get_current_tab()
{
return isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'instagram';
}
/**
* Get tabs configuration
*
* @return array
*/
public function get_tabs()
{
return array(
'instagram' => array(
'title' => __('Instagram', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-instagram',
),
'layout' => array(
'title' => __('Layout', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-grid-view',
),
'display' => array(
'title' => __('Display', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-visibility',
),
'styling' => array(
'title' => __('Styling', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-art',
),
'advanced' => array(
'title' => __('Advanced', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-admin-generic',
),
);
}
}

404
includes/class-cron.php Normal file
View File

@@ -0,0 +1,404 @@
<?php
/**
* Cron Handler Class
*
* Manages scheduled synchronization tasks.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Cron
*/
class IGSP_Cron
{
/**
* Cron hook name
*
* @var string
*/
const SYNC_HOOK = 'igsp_sync_instagram';
/**
* Cleanup hook name
*
* @var string
*/
const CLEANUP_HOOK = 'igsp_cleanup_logs';
/**
* Lock transient name
*
* @var string
*/
const LOCK_KEY = 'igsp_sync_lock';
/**
* Logger instance
*
* @var IGSP_Logger
*/
private $logger;
/**
* Constructor
*/
public function __construct()
{
$this->logger = new IGSP_Logger();
}
/**
* Initialize cron handlers
*
* @return void
*/
public function init()
{
// Register custom intervals
add_filter('cron_schedules', array($this, 'add_custom_intervals'));
// Register cron hooks
add_action(self::SYNC_HOOK, array($this, 'run_sync'));
add_action(self::CLEANUP_HOOK, array($this, 'run_cleanup'));
}
/**
* Add custom cron intervals
*
* @param array $schedules Existing schedules
* @return array
*/
public function add_custom_intervals($schedules)
{
$schedules['igsp_30min'] = array(
'interval' => 30 * MINUTE_IN_SECONDS,
'display' => __('Every 30 Minutes', 'instagram-gallery-sync-pro'),
);
$schedules['igsp_6hours'] = array(
'interval' => 6 * HOUR_IN_SECONDS,
'display' => __('Every 6 Hours', 'instagram-gallery-sync-pro'),
);
return $schedules;
}
/**
* Get available sync intervals
*
* @return array
*/
public function get_intervals()
{
return array(
'igsp_30min' => __('Every 30 Minutes', 'instagram-gallery-sync-pro'),
'hourly' => __('Hourly', 'instagram-gallery-sync-pro'),
'igsp_6hours' => __('Every 6 Hours', 'instagram-gallery-sync-pro'),
'daily' => __('Daily', 'instagram-gallery-sync-pro'),
'weekly' => __('Weekly', 'instagram-gallery-sync-pro'),
);
}
/**
* Schedule sync job
*
* @return void
*/
public function schedule_sync()
{
// Get interval setting
$interval = get_option('igsp_sync_interval', 'daily');
// Clear existing schedule
$this->unschedule_sync();
// Schedule new sync
if (!wp_next_scheduled(self::SYNC_HOOK)) {
wp_schedule_event(time(), $interval, self::SYNC_HOOK);
$this->logger->info(sprintf(__('Scheduled sync with interval: %s', 'instagram-gallery-sync-pro'), $interval));
}
// Schedule daily cleanup if not set
if (!wp_next_scheduled(self::CLEANUP_HOOK)) {
wp_schedule_event(time(), 'daily', self::CLEANUP_HOOK);
}
}
/**
* Unschedule sync job
*
* @return void
*/
public function unschedule_sync()
{
$timestamp = wp_next_scheduled(self::SYNC_HOOK);
if ($timestamp) {
wp_unschedule_event($timestamp, self::SYNC_HOOK);
}
wp_clear_scheduled_hook(self::SYNC_HOOK);
}
/**
* Check if sync is scheduled
*
* @return bool
*/
public function is_scheduled()
{
return (bool) wp_next_scheduled(self::SYNC_HOOK);
}
/**
* Get next scheduled sync time
*
* @return int|false Timestamp or false
*/
public function get_next_sync()
{
return wp_next_scheduled(self::SYNC_HOOK);
}
/**
* Run the sync process
*
* @param bool $manual Whether this is a manual sync
* @return array Result data
*/
public function run_sync($manual = false)
{
// Check if already running
if ($this->is_locked()) {
$this->logger->warning(__('Sync already in progress, skipping.', 'instagram-gallery-sync-pro'));
return array(
'success' => false,
'message' => __('Sync already in progress.', 'instagram-gallery-sync-pro'),
);
}
// Acquire lock
$this->acquire_lock();
$result = array(
'success' => false,
'message' => '',
'posts_added' => 0,
'posts_updated' => 0,
'posts_skipped' => 0,
'errors' => array(),
);
try {
// Get username
$username = get_option('igsp_username', '');
if (empty($username)) {
throw new Exception(__('No Instagram username configured.', 'instagram-gallery-sync-pro'));
}
$this->logger->info(sprintf(__('Starting sync for @%s', 'instagram-gallery-sync-pro'), $username));
// Initialize components
$scraper = new IGSP_Scraper();
$database = new IGSP_Database();
$image_handler = new IGSP_Image_Handler();
// Fetch posts from Instagram
$posts = $scraper->fetch_profile_data($username);
if (is_wp_error($posts)) {
throw new Exception($posts->get_error_message());
}
if (empty($posts)) {
throw new Exception(__('No posts found.', 'instagram-gallery-sync-pro'));
}
// Get existing IDs
$existing_ids = $database->get_existing_instagram_ids();
$save_locally = get_option('igsp_save_locally', 'yes') === 'yes';
// Process each post
foreach ($posts as $post) {
// Validate post data
if (!$scraper->validate_post_data($post)) {
$result['posts_skipped']++;
continue;
}
$is_new = !in_array($post['instagram_id'], $existing_ids, true);
// Prepare post data
$post_data = array(
'instagram_id' => $post['instagram_id'],
'username' => $post['username'],
'post_url' => $post['post_url'],
'caption' => $post['caption'],
'likes_count' => $post['likes_count'],
'comments_count' => $post['comments_count'],
'posted_at' => $post['posted_at'],
'image_width' => $post['image_width'],
'image_height' => $post['image_height'],
);
// Download image if saving locally and is new post
if ($save_locally && $is_new && !empty($post['image_url'])) {
$image_result = $image_handler->download_image($post['image_url'], $post['instagram_id']);
if (!is_wp_error($image_result)) {
$post_data['image_local_path'] = $image_result['local_path'];
$post_data['image_thumbnail_path'] = $image_result['thumbnail_path'];
$post_data['file_size'] = $image_result['file_size'];
$post_data['image_width'] = $image_result['width'];
$post_data['image_height'] = $image_result['height'];
} else {
$result['errors'][] = sprintf(
__('Failed to download image for post %s: %s', 'instagram-gallery-sync-pro'),
$post['instagram_id'],
$image_result->get_error_message()
);
}
}
// Save to database
$saved = $database->insert_post($post_data);
if ($saved) {
if ($is_new) {
$result['posts_added']++;
} else {
$result['posts_updated']++;
}
} else {
$result['errors'][] = sprintf(
__('Failed to save post %s to database.', 'instagram-gallery-sync-pro'),
$post['instagram_id']
);
}
}
// Update last sync time
update_option('igsp_last_sync', current_time('mysql'));
// Delete old posts if auto-delete is enabled
$auto_delete_days = (int) get_option('igsp_auto_delete_days', 0);
if ($auto_delete_days > 0) {
$deleted = $database->delete_old_posts($auto_delete_days);
if ($deleted > 0) {
$this->logger->info(sprintf(__('Auto-deleted %d old posts.', 'instagram-gallery-sync-pro'), $deleted));
}
}
// Clear cache
igsp()->clear_transients();
$result['success'] = true;
$result['message'] = sprintf(
__('Sync completed. Added: %d, Updated: %d, Skipped: %d', 'instagram-gallery-sync-pro'),
$result['posts_added'],
$result['posts_updated'],
$result['posts_skipped']
);
$this->logger->success($result['message'], array(
'added' => $result['posts_added'],
'updated' => $result['posts_updated'],
'skipped' => $result['posts_skipped'],
));
} catch (Exception $e) {
$result['message'] = $e->getMessage();
$result['errors'][] = $e->getMessage();
$this->logger->error(__('Sync failed: ', 'instagram-gallery-sync-pro') . $e->getMessage());
}
// Release lock
$this->release_lock();
return $result;
}
/**
* Run cleanup tasks
*
* @return void
*/
public function run_cleanup()
{
$logger = new IGSP_Logger();
// Cleanup old logs (keep last 7 days)
$logger->delete_logs_older_than(7);
// Other cleanup tasks can be added here
}
/**
* Check if sync is locked
*
* @return bool
*/
private function is_locked()
{
return (bool) get_transient(self::LOCK_KEY);
}
/**
* Acquire sync lock
*
* @return void
*/
private function acquire_lock()
{
set_transient(self::LOCK_KEY, true, 10 * MINUTE_IN_SECONDS);
}
/**
* Release sync lock
*
* @return void
*/
private function release_lock()
{
delete_transient(self::LOCK_KEY);
}
/**
* Manually trigger sync
*
* @return array
*/
public function manual_sync()
{
return $this->run_sync(true);
}
/**
* Get sync status for AJAX
*
* @return array
*/
public function get_sync_status()
{
$last_sync = get_option('igsp_last_sync', '');
$next_sync = $this->get_next_sync();
$is_running = $this->is_locked();
$database = new IGSP_Database();
return array(
'is_running' => $is_running,
'last_sync' => $last_sync ? mysql2date(get_option('date_format') . ' ' . get_option('time_format'), $last_sync) : __('Never', 'instagram-gallery-sync-pro'),
'next_sync' => $next_sync ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_sync) : __('Not scheduled', 'instagram-gallery-sync-pro'),
'total_posts' => $database->count_posts(),
);
}
}

459
includes/class-database.php Normal file
View File

@@ -0,0 +1,459 @@
<?php
/**
* Database Handler Class
*
* Handles all database operations including table creation,
* CRUD operations for Instagram posts and logging.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Database
*/
class IGSP_Database
{
/**
* Database version
*
* @var string
*/
private $db_version = '1.0.0';
/**
* Constructor
*/
public function __construct()
{
// Check for database updates on admin init
add_action('admin_init', array($this, 'maybe_upgrade'));
}
/**
* Create database tables
*
* @return void
*/
public function create_tables()
{
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
// Posts table
$posts_table = IGSP_TABLE_POSTS;
$sql_posts = "CREATE TABLE {$posts_table} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
instagram_id VARCHAR(255) NOT NULL,
username VARCHAR(255) NOT NULL,
image_local_path VARCHAR(500) DEFAULT NULL,
image_thumbnail_path VARCHAR(500) DEFAULT NULL,
post_url VARCHAR(500) DEFAULT NULL,
caption TEXT DEFAULT NULL,
likes_count INT(11) DEFAULT NULL,
comments_count INT(11) DEFAULT NULL,
posted_at DATETIME DEFAULT NULL,
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
file_size INT(11) DEFAULT 0,
image_width INT(11) DEFAULT 0,
image_height INT(11) DEFAULT 0,
is_active TINYINT(1) DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY instagram_id (instagram_id),
KEY username (username),
KEY posted_at (posted_at),
KEY is_active (is_active)
) {$charset_collate};";
// Log table
$log_table = IGSP_TABLE_LOG;
$sql_log = "CREATE TABLE {$log_table} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
log_type VARCHAR(50) NOT NULL DEFAULT 'info',
message TEXT NOT NULL,
details LONGTEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY log_type (log_type),
KEY created_at (created_at)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql_posts);
dbDelta($sql_log);
// Store database version
update_option('igsp_db_version', $this->db_version);
}
/**
* Check and run database upgrades if needed
*
* @return void
*/
public function maybe_upgrade()
{
$installed_version = get_option('igsp_db_version', '0');
if (version_compare($installed_version, $this->db_version, '<')) {
$this->create_tables();
}
}
/**
* Insert or update a post
*
* @param array $data Post data
* @return int|false Insert ID or false on failure
*/
public function insert_post($data)
{
global $wpdb;
// Check if post already exists
$existing = $this->get_post_by_instagram_id($data['instagram_id']);
if ($existing) {
// Update existing post
return $this->update_post($existing->id, $data);
}
// Sanitize data
$insert_data = array(
'instagram_id' => sanitize_text_field($data['instagram_id']),
'username' => sanitize_text_field($data['username']),
'image_local_path' => isset($data['image_local_path']) ? sanitize_text_field($data['image_local_path']) : '',
'image_thumbnail_path' => isset($data['image_thumbnail_path']) ? sanitize_text_field($data['image_thumbnail_path']) : '',
'post_url' => isset($data['post_url']) ? esc_url_raw($data['post_url']) : '',
'caption' => isset($data['caption']) ? wp_kses_post($data['caption']) : '',
'likes_count' => isset($data['likes_count']) ? absint($data['likes_count']) : null,
'comments_count' => isset($data['comments_count']) ? absint($data['comments_count']) : null,
'posted_at' => isset($data['posted_at']) ? sanitize_text_field($data['posted_at']) : current_time('mysql'),
'synced_at' => current_time('mysql'),
'file_size' => isset($data['file_size']) ? absint($data['file_size']) : 0,
'image_width' => isset($data['image_width']) ? absint($data['image_width']) : 0,
'image_height' => isset($data['image_height']) ? absint($data['image_height']) : 0,
'is_active' => isset($data['is_active']) ? absint($data['is_active']) : 1,
);
$result = $wpdb->insert(
IGSP_TABLE_POSTS,
$insert_data,
array(
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%d',
'%d',
'%s',
'%s',
'%d',
'%d',
'%d',
'%d'
)
);
return $result ? $wpdb->insert_id : false;
}
/**
* Update a post
*
* @param int $id Post ID
* @param array $data Post data
* @return bool
*/
public function update_post($id, $data)
{
global $wpdb;
$update_data = array();
$formats = array();
// Map of fields and their formats
$field_map = array(
'image_local_path' => '%s',
'image_thumbnail_path' => '%s',
'post_url' => '%s',
'caption' => '%s',
'likes_count' => '%d',
'comments_count' => '%d',
'posted_at' => '%s',
'file_size' => '%d',
'image_width' => '%d',
'image_height' => '%d',
'is_active' => '%d',
);
foreach ($field_map as $field => $format) {
if (isset($data[$field])) {
$update_data[$field] = $data[$field];
$formats[] = $format;
}
}
// Always update synced_at
$update_data['synced_at'] = current_time('mysql');
$formats[] = '%s';
return $wpdb->update(
IGSP_TABLE_POSTS,
$update_data,
array('id' => $id),
$formats,
array('%d')
) !== false;
}
/**
* Get post by Instagram ID
*
* @param string $instagram_id Instagram post ID
* @return object|null
*/
public function get_post_by_instagram_id($instagram_id)
{
global $wpdb;
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE instagram_id = %s",
$instagram_id
)
);
}
/**
* Get all posts
*
* @param array $args Query arguments
* @return array
*/
public function get_posts($args = array())
{
global $wpdb;
$defaults = array(
'limit' => 12,
'offset' => 0,
'order' => 'newest',
'active' => true,
'username' => '',
);
$args = wp_parse_args($args, $defaults);
// Build query
$where = array('1=1');
$values = array();
if ($args['active']) {
$where[] = 'is_active = 1';
}
if (!empty($args['username'])) {
$where[] = 'username = %s';
$values[] = $args['username'];
}
// Order
switch ($args['order']) {
case 'oldest':
$order = 'posted_at ASC';
break;
case 'random':
$order = 'RAND()';
break;
case 'newest':
default:
$order = 'posted_at DESC';
break;
}
$where_clause = implode(' AND ', $where);
// Handle limit
$limit_clause = '';
if ($args['limit'] > 0) {
$limit_clause = $wpdb->prepare('LIMIT %d OFFSET %d', $args['limit'], $args['offset']);
}
$query = "SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE {$where_clause} ORDER BY {$order} {$limit_clause}";
if (!empty($values)) {
$query = $wpdb->prepare($query, $values);
}
return $wpdb->get_results($query);
}
/**
* Count posts
*
* @param bool $active_only Count only active posts
* @return int
*/
public function count_posts($active_only = true)
{
global $wpdb;
$where = $active_only ? 'WHERE is_active = 1' : '';
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM " . IGSP_TABLE_POSTS . " {$where}"
);
}
/**
* Delete post
*
* @param int $id Post ID
* @return bool
*/
public function delete_post($id)
{
global $wpdb;
// Get post for file cleanup
$post = $this->get_post($id);
if ($post) {
// Delete associated files
$this->delete_post_files($post);
}
return $wpdb->delete(
IGSP_TABLE_POSTS,
array('id' => $id),
array('%d')
) !== false;
}
/**
* Get single post by ID
*
* @param int $id Post ID
* @return object|null
*/
public function get_post($id)
{
global $wpdb;
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE id = %d",
$id
)
);
}
/**
* Delete post image files
*
* @param object $post Post object
* @return void
*/
private function delete_post_files($post)
{
$upload_dir = wp_upload_dir();
$base_path = $upload_dir['basedir'];
// Delete main image
if (!empty($post->image_local_path)) {
$file_path = $base_path . '/' . $post->image_local_path;
if (file_exists($file_path)) {
unlink($file_path);
}
}
// Delete thumbnail
if (!empty($post->image_thumbnail_path)) {
$thumb_path = $base_path . '/' . $post->image_thumbnail_path;
if (file_exists($thumb_path)) {
unlink($thumb_path);
}
}
}
/**
* Delete old posts
*
* @param int $days Delete posts older than X days
* @return int Number of deleted posts
*/
public function delete_old_posts($days)
{
global $wpdb;
if ($days <= 0) {
return 0;
}
$date_threshold = date('Y-m-d H:i:s', strtotime("-{$days} days"));
// Get posts to delete for file cleanup
$posts = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM " . IGSP_TABLE_POSTS . " WHERE synced_at < %s",
$date_threshold
)
);
foreach ($posts as $post) {
$this->delete_post_files($post);
}
// Delete from database
return $wpdb->query(
$wpdb->prepare(
"DELETE FROM " . IGSP_TABLE_POSTS . " WHERE synced_at < %s",
$date_threshold
)
);
}
/**
* Delete all posts
*
* @return bool
*/
public function delete_all_posts()
{
global $wpdb;
// Get all posts for file cleanup
$posts = $wpdb->get_results("SELECT * FROM " . IGSP_TABLE_POSTS);
foreach ($posts as $post) {
$this->delete_post_files($post);
}
// Truncate table
return $wpdb->query("TRUNCATE TABLE " . IGSP_TABLE_POSTS) !== false;
}
/**
* Get existing Instagram IDs
*
* @return array
*/
public function get_existing_instagram_ids()
{
global $wpdb;
return $wpdb->get_col(
"SELECT instagram_id FROM " . IGSP_TABLE_POSTS
);
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Gutenberg Block Class
*
* Handles the Instagram Gallery Gutenberg block.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Gutenberg_Block
*/
class IGSP_Gutenberg_Block
{
/**
* Initialize block
*
* @return void
*/
public function init()
{
add_action('init', array($this, 'register_block'));
add_action('enqueue_block_editor_assets', array($this, 'enqueue_editor_assets'));
}
/**
* Register block
*
* @return void
*/
public function register_block()
{
if (!function_exists('register_block_type')) {
return;
}
register_block_type('igsp/instagram-gallery', array(
'editor_script' => 'igsp-block-editor',
'editor_style' => 'igsp-block-editor-style',
'render_callback' => array($this, 'render_block'),
'attributes' => $this->get_block_attributes(),
));
}
/**
* Get block attributes
*
* @return array
*/
private function get_block_attributes()
{
return array(
'layout' => array(
'type' => 'string',
'default' => get_option('igsp_layout_type', 'grid'),
),
'columns' => array(
'type' => 'number',
'default' => get_option('igsp_columns_desktop', 3),
),
'spacing' => array(
'type' => 'number',
'default' => get_option('igsp_spacing', 10),
),
'limit' => array(
'type' => 'number',
'default' => get_option('igsp_display_limit', 12),
),
'order' => array(
'type' => 'string',
'default' => get_option('igsp_order', 'newest'),
),
'lightbox' => array(
'type' => 'boolean',
'default' => get_option('igsp_lightbox', 'yes') === 'yes',
),
'captions' => array(
'type' => 'boolean',
'default' => get_option('igsp_show_caption', 'no') === 'yes',
),
'className' => array(
'type' => 'string',
'default' => '',
),
);
}
/**
* Enqueue editor assets
*
* @return void
*/
public function enqueue_editor_assets()
{
// Editor script
wp_enqueue_script(
'igsp-block-editor',
IGSP_PLUGIN_URL . 'public/js/block-editor.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-server-side-render'),
IGSP_VERSION
);
// Editor style
wp_enqueue_style(
'igsp-block-editor-style',
IGSP_PLUGIN_URL . 'public/css/block-editor.css',
array(),
IGSP_VERSION
);
// Localize
wp_localize_script('igsp-block-editor', 'igspBlock', array(
'pluginUrl' => IGSP_PLUGIN_URL,
'defaultLayout' => get_option('igsp_layout_type', 'grid'),
'layouts' => array(
array('value' => 'grid', 'label' => __('Grid', 'instagram-gallery-sync-pro')),
array('value' => 'masonry', 'label' => __('Masonry', 'instagram-gallery-sync-pro')),
array('value' => 'slider', 'label' => __('Slider', 'instagram-gallery-sync-pro')),
array('value' => 'justified', 'label' => __('Justified', 'instagram-gallery-sync-pro')),
array('value' => 'list', 'label' => __('List', 'instagram-gallery-sync-pro')),
),
'orders' => array(
array('value' => 'newest', 'label' => __('Newest First', 'instagram-gallery-sync-pro')),
array('value' => 'oldest', 'label' => __('Oldest First', 'instagram-gallery-sync-pro')),
array('value' => 'random', 'label' => __('Random', 'instagram-gallery-sync-pro')),
),
'strings' => array(
'title' => __('Instagram Gallery', 'instagram-gallery-sync-pro'),
'description' => __('Display your Instagram photos in a beautiful gallery.', 'instagram-gallery-sync-pro'),
'layout' => __('Layout', 'instagram-gallery-sync-pro'),
'columns' => __('Columns', 'instagram-gallery-sync-pro'),
'spacing' => __('Spacing', 'instagram-gallery-sync-pro'),
'limit' => __('Number of Images', 'instagram-gallery-sync-pro'),
'order' => __('Order', 'instagram-gallery-sync-pro'),
'lightbox' => __('Enable Lightbox', 'instagram-gallery-sync-pro'),
'captions' => __('Show Captions', 'instagram-gallery-sync-pro'),
),
));
}
/**
* Render block
*
* @param array $attributes Block attributes
* @return string
*/
public function render_block($attributes)
{
// Convert block attributes to shortcode attributes
$atts = array(
'layout' => $attributes['layout'] ?? 'grid',
'columns' => $attributes['columns'] ?? 3,
'spacing' => $attributes['spacing'] ?? 10,
'limit' => $attributes['limit'] ?? 12,
'order' => $attributes['order'] ?? 'newest',
'lightbox' => $attributes['lightbox'] ? 'yes' : 'no',
'captions' => $attributes['captions'] ? 'yes' : 'no',
'class' => $attributes['className'] ?? '',
);
// Use shortcode handler for rendering
$shortcode = new IGSP_Shortcode();
return $shortcode->render($atts);
}
}

View File

@@ -0,0 +1,477 @@
<?php
/**
* Image Handler Class
*
* Handles downloading, saving, and processing of Instagram images.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Image_Handler
*/
class IGSP_Image_Handler
{
/**
* Upload directory path
*
* @var string
*/
private $upload_path;
/**
* Upload directory URL
*
* @var string
*/
private $upload_url;
/**
* Thumbnail sizes
*
* @var array
*/
private $thumbnail_sizes = array(
'small' => 150,
'medium' => 320,
'large' => 640,
);
/**
* Logger instance
*
* @var IGSP_Logger
*/
private $logger;
/**
* Constructor
*/
public function __construct()
{
$this->setup_paths();
$this->logger = new IGSP_Logger();
}
/**
* Setup upload paths
*
* @return void
*/
private function setup_paths()
{
$upload_dir = wp_upload_dir();
$this->upload_path = $upload_dir['basedir'] . '/' . IGSP_UPLOAD_DIR;
$this->upload_url = $upload_dir['baseurl'] . '/' . IGSP_UPLOAD_DIR;
// Ensure directory exists
if (!file_exists($this->upload_path)) {
wp_mkdir_p($this->upload_path);
}
}
/**
* Download and save an image
*
* @param string $url Remote image URL
* @param string $instagram_id Instagram post ID for naming
* @return array|WP_Error Array with paths or WP_Error
*/
public function download_image($url, $instagram_id)
{
if (empty($url) || empty($instagram_id)) {
return new WP_Error('invalid_params', __('Invalid parameters for image download.', 'instagram-gallery-sync-pro'));
}
// Sanitize ID for filename
$safe_id = sanitize_file_name($instagram_id);
// Download the image
$temp_file = download_url($url, 30);
if (is_wp_error($temp_file)) {
$this->logger->error(
sprintf(__('Failed to download image: %s', 'instagram-gallery-sync-pro'), $temp_file->get_error_message()),
array('url' => $url)
);
return $temp_file;
}
// Validate the downloaded file
$validation = $this->validate_image($temp_file);
if (is_wp_error($validation)) {
@unlink($temp_file);
return $validation;
}
// Get file info
$file_info = $validation;
$extension = $file_info['extension'];
// Generate filenames
$filename = $safe_id . '.' . $extension;
$filepath = $this->upload_path . '/' . $filename;
// Move file to upload directory
$moved = rename($temp_file, $filepath);
if (!$moved) {
@unlink($temp_file);
return new WP_Error('move_failed', __('Failed to move downloaded file.', 'instagram-gallery-sync-pro'));
}
// Set correct permissions
chmod($filepath, 0644);
// Generate thumbnails
$thumbnail_path = $this->generate_thumbnail($filepath, $safe_id, $extension);
// Get image dimensions
$dimensions = getimagesize($filepath);
// Prepare result
$result = array(
'local_path' => IGSP_UPLOAD_DIR . '/' . $filename,
'thumbnail_path' => $thumbnail_path,
'full_url' => $this->upload_url . '/' . $filename,
'thumbnail_url' => !empty($thumbnail_path) ? $this->upload_url . '/' . str_replace(IGSP_UPLOAD_DIR . '/', '', $thumbnail_path) : '',
'file_size' => filesize($filepath),
'width' => $dimensions ? $dimensions[0] : 0,
'height' => $dimensions ? $dimensions[1] : 0,
'mime_type' => $file_info['mime_type'],
);
$this->logger->info(
sprintf(__('Image saved: %s', 'instagram-gallery-sync-pro'), $filename),
array('size' => $result['file_size'], 'dimensions' => $dimensions[0] . 'x' . $dimensions[1])
);
return $result;
}
/**
* Validate downloaded image
*
* @param string $filepath Path to temp file
* @return array|WP_Error File info or error
*/
private function validate_image($filepath)
{
if (!file_exists($filepath)) {
return new WP_Error('file_not_found', __('Downloaded file not found.', 'instagram-gallery-sync-pro'));
}
// Check file size (max 20MB)
$max_size = 20 * 1024 * 1024;
if (filesize($filepath) > $max_size) {
return new WP_Error('file_too_large', __('File exceeds maximum size limit.', 'instagram-gallery-sync-pro'));
}
// Verify it's actually an image
$file_info = wp_check_filetype_and_ext($filepath, basename($filepath));
$image_info = @getimagesize($filepath);
if (!$image_info) {
return new WP_Error('not_an_image', __('File is not a valid image.', 'instagram-gallery-sync-pro'));
}
// Check allowed types
$allowed_types = array('image/jpeg', 'image/png', 'image/gif', 'image/webp');
$mime_type = $image_info['mime'];
if (!in_array($mime_type, $allowed_types, true)) {
return new WP_Error('invalid_type', __('Image type not allowed.', 'instagram-gallery-sync-pro'));
}
// Map mime to extension
$ext_map = array(
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
);
return array(
'mime_type' => $mime_type,
'extension' => $ext_map[$mime_type] ?? 'jpg',
'width' => $image_info[0],
'height' => $image_info[1],
);
}
/**
* Generate thumbnail from image
*
* @param string $filepath Original image path
* @param string $safe_id Sanitized ID for naming
* @param string $extension File extension
* @return string|null Thumbnail path or null on failure
*/
public function generate_thumbnail($filepath, $safe_id, $extension)
{
// Use WordPress image editor
$editor = wp_get_image_editor($filepath);
if (is_wp_error($editor)) {
$this->logger->warning(
sprintf(__('Could not create image editor: %s', 'instagram-gallery-sync-pro'), $editor->get_error_message())
);
return null;
}
// Get current size
$size = $editor->get_size();
// Calculate thumbnail size (medium by default)
$thumb_size = $this->thumbnail_sizes['medium'];
// Resize while maintaining aspect ratio
$editor->resize($thumb_size, $thumb_size, true); // true = crop
// Optimize quality
$editor->set_quality(85);
// Save thumbnail
$thumb_dir = $this->upload_path . '/thumbnails';
if (!file_exists($thumb_dir)) {
wp_mkdir_p($thumb_dir);
}
$thumb_filename = $safe_id . '-thumb.' . $extension;
$thumb_path = $thumb_dir . '/' . $thumb_filename;
$saved = $editor->save($thumb_path);
if (is_wp_error($saved)) {
$this->logger->warning(
sprintf(__('Failed to save thumbnail: %s', 'instagram-gallery-sync-pro'), $saved->get_error_message())
);
return null;
}
return IGSP_UPLOAD_DIR . '/thumbnails/' . $thumb_filename;
}
/**
* Convert image to WebP (optional)
*
* @param string $filepath Original image path
* @return string|null WebP path or null
*/
public function convert_to_webp($filepath)
{
if (!function_exists('imagewebp')) {
return null;
}
$image_info = getimagesize($filepath);
if (!$image_info) {
return null;
}
$mime = $image_info['mime'];
// Create image resource based on type
switch ($mime) {
case 'image/jpeg':
$image = imagecreatefromjpeg($filepath);
break;
case 'image/png':
$image = imagecreatefrompng($filepath);
break;
case 'image/gif':
$image = imagecreatefromgif($filepath);
break;
default:
return null;
}
if (!$image) {
return null;
}
// Generate WebP path
$webp_path = preg_replace('/\.(jpe?g|png|gif)$/i', '.webp', $filepath);
// Save as WebP
$success = imagewebp($image, $webp_path, 85);
imagedestroy($image);
if (!$success) {
return null;
}
return str_replace($this->upload_path, IGSP_UPLOAD_DIR, $webp_path);
}
/**
* Strip EXIF data from image
*
* @param string $filepath Image path
* @return bool
*/
public function strip_exif($filepath)
{
$image_info = getimagesize($filepath);
if (!$image_info || $image_info['mime'] !== 'image/jpeg') {
return false;
}
$image = imagecreatefromjpeg($filepath);
if (!$image) {
return false;
}
// Re-save without EXIF
$result = imagejpeg($image, $filepath, 90);
imagedestroy($image);
return $result;
}
/**
* Delete image files for a post
*
* @param string $local_path Main image path
* @param string $thumbnail_path Thumbnail path
* @return bool
*/
public function delete_images($local_path, $thumbnail_path = '')
{
$upload_dir = wp_upload_dir();
$base_path = $upload_dir['basedir'];
$deleted = false;
if (!empty($local_path)) {
$full_path = $base_path . '/' . $local_path;
if (file_exists($full_path) && strpos($full_path, IGSP_UPLOAD_DIR) !== false) {
$deleted = unlink($full_path);
}
}
if (!empty($thumbnail_path)) {
$thumb_full_path = $base_path . '/' . $thumbnail_path;
if (file_exists($thumb_full_path) && strpos($thumb_full_path, IGSP_UPLOAD_DIR) !== false) {
unlink($thumb_full_path);
}
}
return $deleted;
}
/**
* Get image URL from local path
*
* @param string $local_path Local path
* @return string
*/
public function get_image_url($local_path)
{
if (empty($local_path)) {
return '';
}
$upload_dir = wp_upload_dir();
return $upload_dir['baseurl'] . '/' . $local_path;
}
/**
* Check if image exists locally
*
* @param string $local_path Local path
* @return bool
*/
public function image_exists($local_path)
{
if (empty($local_path)) {
return false;
}
$upload_dir = wp_upload_dir();
$full_path = $upload_dir['basedir'] . '/' . $local_path;
return file_exists($full_path);
}
/**
* Get total size of all stored images
*
* @return int Size in bytes
*/
public function get_total_storage_used()
{
$total = 0;
if (!is_dir($this->upload_path)) {
return $total;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->upload_path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
$total += $file->getSize();
}
return $total;
}
/**
* Get human-readable storage size
*
* @return string
*/
public function get_storage_used_formatted()
{
$bytes = $this->get_total_storage_used();
return size_format($bytes);
}
/**
* Clear all stored images
*
* @return bool
*/
public function clear_all_images()
{
if (!is_dir($this->upload_path)) {
return true;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->upload_path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item) {
if ($item->isDir()) {
rmdir($item->getRealPath());
} else {
// Don't delete index.php and .htaccess
$filename = $item->getFilename();
if ($filename !== 'index.php' && $filename !== '.htaccess') {
unlink($item->getRealPath());
}
}
}
// Recreate thumbnails directory
wp_mkdir_p($this->upload_path . '/thumbnails');
return true;
}
}

331
includes/class-logger.php Normal file
View File

@@ -0,0 +1,331 @@
<?php
/**
* Logger Class
*
* Handles logging of plugin events, errors, and debugging information.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Logger
*/
class IGSP_Logger
{
/**
* Log types
*/
const TYPE_SUCCESS = 'success';
const TYPE_ERROR = 'error';
const TYPE_WARNING = 'warning';
const TYPE_INFO = 'info';
/**
* Maximum log entries to keep
*
* @var int
*/
private $max_entries = 500;
/**
* Constructor
*/
public function __construct()
{
// Schedule log cleanup
add_action('igsp_cleanup_logs', array($this, 'cleanup_old_logs'));
}
/**
* Add a log entry
*
* @param string $type Log type (success, error, warning, info)
* @param string $message Log message
* @param array $details Additional details (optional)
* @return int|false Insert ID or false
*/
public function log($type, $message, $details = array())
{
global $wpdb;
// Validate type
$valid_types = array(self::TYPE_SUCCESS, self::TYPE_ERROR, self::TYPE_WARNING, self::TYPE_INFO);
if (!in_array($type, $valid_types, true)) {
$type = self::TYPE_INFO;
}
// Only log in debug mode unless it's an error
$debug_mode = get_option('igsp_debug_mode', 'no');
if ($debug_mode !== 'yes' && $type !== self::TYPE_ERROR) {
return false;
}
$result = $wpdb->insert(
IGSP_TABLE_LOG,
array(
'log_type' => $type,
'message' => sanitize_text_field($message),
'details' => !empty($details) ? wp_json_encode($details) : null,
'created_at' => current_time('mysql'),
),
array('%s', '%s', '%s', '%s')
);
// Also log to error_log if debug is enabled
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf(
'[IGSP %s] %s %s',
strtoupper($type),
$message,
!empty($details) ? wp_json_encode($details) : ''
));
}
return $result ? $wpdb->insert_id : false;
}
/**
* Log success
*
* @param string $message Message
* @param array $details Details
* @return int|false
*/
public function success($message, $details = array())
{
return $this->log(self::TYPE_SUCCESS, $message, $details);
}
/**
* Log error
*
* @param string $message Message
* @param array $details Details
* @return int|false
*/
public function error($message, $details = array())
{
return $this->log(self::TYPE_ERROR, $message, $details);
}
/**
* Log warning
*
* @param string $message Message
* @param array $details Details
* @return int|false
*/
public function warning($message, $details = array())
{
return $this->log(self::TYPE_WARNING, $message, $details);
}
/**
* Log info
*
* @param string $message Message
* @param array $details Details
* @return int|false
*/
public function info($message, $details = array())
{
return $this->log(self::TYPE_INFO, $message, $details);
}
/**
* Get log entries
*
* @param array $args Query arguments
* @return array
*/
public function get_logs($args = array())
{
global $wpdb;
$defaults = array(
'limit' => 50,
'offset' => 0,
'type' => '',
'search' => '',
);
$args = wp_parse_args($args, $defaults);
$where = array('1=1');
$values = array();
if (!empty($args['type'])) {
$where[] = 'log_type = %s';
$values[] = $args['type'];
}
if (!empty($args['search'])) {
$where[] = 'message LIKE %s';
$values[] = '%' . $wpdb->esc_like($args['search']) . '%';
}
$where_clause = implode(' AND ', $where);
$query = "SELECT * FROM " . IGSP_TABLE_LOG . "
WHERE {$where_clause}
ORDER BY created_at DESC
LIMIT %d OFFSET %d";
$values[] = $args['limit'];
$values[] = $args['offset'];
return $wpdb->get_results(
$wpdb->prepare($query, $values)
);
}
/**
* Count log entries
*
* @param string $type Optional log type filter
* @return int
*/
public function count_logs($type = '')
{
global $wpdb;
if (!empty($type)) {
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM " . IGSP_TABLE_LOG . " WHERE log_type = %s",
$type
)
);
}
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM " . IGSP_TABLE_LOG
);
}
/**
* Clear all logs
*
* @return bool
*/
public function clear_logs()
{
global $wpdb;
return $wpdb->query("TRUNCATE TABLE " . IGSP_TABLE_LOG) !== false;
}
/**
* Cleanup old log entries
*
* @return int Number of deleted entries
*/
public function cleanup_old_logs()
{
global $wpdb;
// Keep only the most recent entries
return $wpdb->query(
$wpdb->prepare(
"DELETE FROM " . IGSP_TABLE_LOG . "
WHERE id NOT IN (
SELECT id FROM (
SELECT id FROM " . IGSP_TABLE_LOG . "
ORDER BY created_at DESC
LIMIT %d
) AS t
)",
$this->max_entries
)
);
}
/**
* Delete logs older than X days
*
* @param int $days Days to keep
* @return int
*/
public function delete_logs_older_than($days)
{
global $wpdb;
$date_threshold = date('Y-m-d H:i:s', strtotime("-{$days} days"));
return $wpdb->query(
$wpdb->prepare(
"DELETE FROM " . IGSP_TABLE_LOG . " WHERE created_at < %s",
$date_threshold
)
);
}
/**
* Get logs formatted for display
*
* @param int $limit Number of entries
* @return array
*/
public function get_formatted_logs($limit = 50)
{
$logs = $this->get_logs(array('limit' => $limit));
$formatted = array();
foreach ($logs as $log) {
$formatted[] = array(
'id' => $log->id,
'type' => $log->log_type,
'type_label' => $this->get_type_label($log->log_type),
'type_class' => $this->get_type_class($log->log_type),
'message' => $log->message,
'details' => !empty($log->details) ? json_decode($log->details, true) : array(),
'date' => mysql2date(get_option('date_format') . ' ' . get_option('time_format'), $log->created_at),
'relative' => human_time_diff(strtotime($log->created_at), current_time('timestamp')) . ' ' . __('ago', 'instagram-gallery-sync-pro'),
);
}
return $formatted;
}
/**
* Get type label
*
* @param string $type Log type
* @return string
*/
private function get_type_label($type)
{
$labels = array(
self::TYPE_SUCCESS => __('Success', 'instagram-gallery-sync-pro'),
self::TYPE_ERROR => __('Error', 'instagram-gallery-sync-pro'),
self::TYPE_WARNING => __('Warning', 'instagram-gallery-sync-pro'),
self::TYPE_INFO => __('Info', 'instagram-gallery-sync-pro'),
);
return isset($labels[$type]) ? $labels[$type] : $type;
}
/**
* Get type CSS class
*
* @param string $type Log type
* @return string
*/
private function get_type_class($type)
{
$classes = array(
self::TYPE_SUCCESS => 'igsp-log-success',
self::TYPE_ERROR => 'igsp-log-error',
self::TYPE_WARNING => 'igsp-log-warning',
self::TYPE_INFO => 'igsp-log-info',
);
return isset($classes[$type]) ? $classes[$type] : 'igsp-log-info';
}
}

938
includes/class-scraper.php Normal file
View File

@@ -0,0 +1,938 @@
<?php
/**
* Instagram Scraper Class
*
* Handles scraping of public Instagram profiles without official API.
* Uses multiple fallback methods and anti-blocking measures.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Scraper
*/
class IGSP_Scraper
{
/**
* Array of User-Agents for rotation
*
* @var array
*/
private $user_agents = array(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
);
/**
* Request timeout in seconds
*
* @var int
*/
private $timeout;
/**
* Maximum retry attempts
*
* @var int
*/
private $max_retries = 3;
/**
* Minimum delay between requests (seconds)
*
* @var int
*/
private $min_delay = 2;
/**
* Maximum delay between requests (seconds)
*
* @var int
*/
private $max_delay = 5;
/**
* Logger instance
*
* @var IGSP_Logger
*/
private $logger;
/**
* Constructor
*/
public function __construct()
{
$this->timeout = (int) get_option('igsp_request_timeout', 30);
$this->logger = new IGSP_Logger();
}
/**
* Fetch Instagram profile data
*
* @param string $username Instagram username
* @return array|WP_Error Array of posts or WP_Error on failure
*/
public function fetch_profile_data($username)
{
$username = sanitize_text_field($username);
if (empty($username)) {
return new WP_Error('no_username', __('Username not provided.', 'instagram-gallery-sync-pro'));
}
// Clean username (remove @ if present)
$username = ltrim($username, '@');
$this->logger->info(sprintf(__('Starting fetch for username: %s', 'instagram-gallery-sync-pro'), $username));
// Try multiple methods in order
$methods = array(
'fetch_via_web_profile_info',
'fetch_via_embed_page',
'fetch_via_profile_page',
);
$last_error = null;
foreach ($methods as $method) {
$this->random_delay();
$result = $this->$method($username);
if (!is_wp_error($result) && !empty($result)) {
$this->logger->success(
sprintf(__('Successfully fetched %d posts using %s', 'instagram-gallery-sync-pro'), count($result), $method),
array('method' => $method, 'count' => count($result))
);
return $result;
}
if (is_wp_error($result)) {
$last_error = $result;
$this->logger->warning(
sprintf(__('Method %s failed: %s', 'instagram-gallery-sync-pro'), $method, $result->get_error_message()),
array('method' => $method)
);
}
}
// All methods failed
$error = $last_error ?? new WP_Error('fetch_failed', __('All scraping methods failed.', 'instagram-gallery-sync-pro'));
$this->logger->error(__('Failed to fetch Instagram data after trying all methods.', 'instagram-gallery-sync-pro'));
return $error;
}
/**
* Fetch via web profile info API
* This uses the internal API that Instagram's web app uses
*
* @param string $username Instagram username
* @return array|WP_Error
*/
private function fetch_via_web_profile_info($username)
{
// First, we need to get the app id and other required headers from the main page
$main_url = 'https://www.instagram.com/';
$main_response = $this->make_request($main_url);
if (is_wp_error($main_response)) {
return $main_response;
}
$main_body = wp_remote_retrieve_body($main_response);
// Extract the app ID
$app_id = $this->extract_app_id($main_body);
$this->random_delay();
// Now fetch the profile using the web profile info endpoint
$url = 'https://www.instagram.com/api/v1/users/web_profile_info/?username=' . urlencode($username);
$headers = array(
'X-IG-App-ID' => $app_id ?: '936619743392459',
'X-ASBD-ID' => '129477',
'X-IG-WWW-Claim' => '0',
'X-Requested-With' => 'XMLHttpRequest',
'Referer' => 'https://www.instagram.com/' . $username . '/',
'Accept' => '*/*',
);
$response = $this->make_request($url, array('headers' => $headers));
if (is_wp_error($response)) {
return $response;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return new WP_Error('json_error', __('Invalid JSON response from web profile info.', 'instagram-gallery-sync-pro'));
}
// Check for user data
if (empty($data['data']['user'])) {
return new WP_Error('no_user', __('No user data found.', 'instagram-gallery-sync-pro'));
}
$user = $data['data']['user'];
// Check if profile is private
if (!empty($user['is_private'])) {
return new WP_Error('private_profile', __('This Instagram profile is private.', 'instagram-gallery-sync-pro'));
}
// Get media
$edges = $user['edge_owner_to_timeline_media']['edges'] ?? array();
if (empty($edges)) {
return new WP_Error('no_media', __('No media found on this profile.', 'instagram-gallery-sync-pro'));
}
return $this->parse_graphql_edges($edges, $username);
}
/**
* Fetch via embed page (iframeable content)
*
* @param string $username Instagram username
* @return array|WP_Error
*/
private function fetch_via_embed_page($username)
{
// First get the profile to find some post shortcodes
$profile_url = 'https://www.instagram.com/' . $username . '/';
$response = $this->make_request($profile_url);
if (is_wp_error($response)) {
return $response;
}
$body = wp_remote_retrieve_body($response);
// Try to extract shortcodes from the HTML
$shortcodes = $this->extract_shortcodes($body);
if (empty($shortcodes)) {
return new WP_Error('no_shortcodes', __('Could not find any post links.', 'instagram-gallery-sync-pro'));
}
$posts = array();
$max_images = min((int) get_option('igsp_max_images', 12), count($shortcodes));
// Fetch each post via embed
for ($i = 0; $i < $max_images; $i++) {
$this->random_delay(1, 2);
$embed_url = 'https://www.instagram.com/p/' . $shortcodes[$i] . '/embed/';
$embed_response = $this->make_request($embed_url);
if (is_wp_error($embed_response)) {
continue;
}
$embed_body = wp_remote_retrieve_body($embed_response);
$post_data = $this->parse_embed_page($embed_body, $shortcodes[$i], $username);
if ($post_data) {
$posts[] = $post_data;
}
}
if (empty($posts)) {
return new WP_Error('no_posts', __('Could not extract any posts from embeds.', 'instagram-gallery-sync-pro'));
}
return $posts;
}
/**
* Fetch via profile page HTML parsing (fallback)
*
* @param string $username Instagram username
* @return array|WP_Error
*/
private function fetch_via_profile_page($username)
{
$url = 'https://www.instagram.com/' . $username . '/';
$response = $this->make_request($url);
if (is_wp_error($response)) {
return $response;
}
$body = wp_remote_retrieve_body($response);
if (empty($body)) {
return new WP_Error('empty_response', __('Empty response from Instagram.', 'instagram-gallery-sync-pro'));
}
// Try multiple parsing strategies
$posts = $this->parse_require_js_data($body, $username);
if (!empty($posts)) {
return $posts;
}
$posts = $this->parse_shared_data($body, $username);
if (!empty($posts)) {
return $posts;
}
$posts = $this->parse_preloaded_data($body, $username);
if (!empty($posts)) {
return $posts;
}
$posts = $this->parse_og_images($body, $username);
if (!empty($posts)) {
return $posts;
}
return new WP_Error('parse_failed', __('Could not parse Instagram profile page.', 'instagram-gallery-sync-pro'));
}
/**
* Extract app ID from Instagram main page
*
* @param string $html HTML content
* @return string|null
*/
private function extract_app_id($html)
{
// Look for the app ID in various places
$patterns = array(
'/"X-IG-App-ID":"(\d+)"/',
'/appId":"(\d+)"/',
'/APP_ID":"(\d+)"/',
'/{"APP_ID":"(\d+)"/',
);
foreach ($patterns as $pattern) {
if (preg_match($pattern, $html, $matches)) {
return $matches[1];
}
}
return null;
}
/**
* Extract shortcodes from profile HTML
*
* @param string $html HTML content
* @return array
*/
private function extract_shortcodes($html)
{
$shortcodes = array();
// Pattern to find post links
$patterns = array(
'/\/p\/([A-Za-z0-9_-]+)\//',
'/\/reel\/([A-Za-z0-9_-]+)\//',
'/"shortcode":"([A-Za-z0-9_-]+)"/',
'/"code":"([A-Za-z0-9_-]+)"/',
);
foreach ($patterns as $pattern) {
if (preg_match_all($pattern, $html, $matches)) {
$shortcodes = array_merge($shortcodes, $matches[1]);
}
}
// Remove duplicates and return
return array_unique($shortcodes);
}
/**
* Parse embed page HTML
*
* @param string $html HTML content
* @param string $shortcode Post shortcode
* @param string $username Username
* @return array|null
*/
private function parse_embed_page($html, $shortcode, $username)
{
// Look for image URL in embed page
$image_url = null;
$caption = '';
// Try to find the main image
$img_patterns = array(
'/<img[^>]+class="[^"]*EmbeddedMediaImage[^"]*"[^>]+src="([^"]+)"/',
'/<img[^>]+src="([^"]+instagram[^"]+)"[^>]+class="[^"]*Image/',
'/property="og:image"[^>]+content="([^"]+)"/',
'/content="([^"]+)"[^>]+property="og:image"/',
'/<img[^>]+srcset="([^"]+)"/',
);
foreach ($img_patterns as $pattern) {
if (preg_match($pattern, $html, $matches)) {
$image_url = $matches[1];
// Handle srcset - get the first URL
if (strpos($image_url, ' ') !== false) {
$image_url = explode(' ', $image_url)[0];
}
break;
}
}
// Try to extract from JSON in the page
if (empty($image_url)) {
if (preg_match('/"display_url":"([^"]+)"/', $html, $matches)) {
$image_url = stripcslashes($matches[1]);
}
}
if (empty($image_url)) {
return null;
}
// Extract caption
if (preg_match('/<div class="[^"]*Caption[^"]*"[^>]*>.*?<span[^>]*>(.+?)<\/span>/s', $html, $matches)) {
$caption = strip_tags($matches[1]);
}
// Try to get timestamp
$timestamp = null;
if (preg_match('/datetime="([^"]+)"/', $html, $matches)) {
$timestamp = strtotime($matches[1]);
}
return array(
'instagram_id' => md5($shortcode . $username),
'shortcode' => $shortcode,
'username' => $username,
'image_url' => $image_url,
'thumbnail_url' => $image_url,
'post_url' => 'https://www.instagram.com/p/' . $shortcode . '/',
'caption' => $caption,
'likes_count' => null,
'comments_count' => null,
'posted_at' => $timestamp ? date('Y-m-d H:i:s', $timestamp) : current_time('mysql'),
'image_width' => 0,
'image_height' => 0,
'is_video' => false,
);
}
/**
* Parse require.js data from modern Instagram
*
* @param string $html HTML content
* @param string $username Username
* @return array
*/
private function parse_require_js_data($html, $username)
{
// Modern Instagram uses require.js with data in script tags
$pattern = '/requireLazy\(\["JSScheduler"[^}]+},\s*function\(\)[^{]*{\s*"use strict";\s*(.+?)\s*}\s*\)/s';
// Try to find JSON data in script tags
$json_patterns = array(
'/"xdt_api__v1__feed__user_timeline_graphql_connection":\s*({.+?})\s*,\s*"extensions"/',
'/"edge_owner_to_timeline_media":\s*({.+?})\s*,\s*"edge_/',
'/{"xdt_api__v1__users__web_profile_info".*?"user":\s*({.+?})\s*}\s*}/',
);
foreach ($json_patterns as $pattern) {
if (preg_match($pattern, $html, $matches)) {
$data = json_decode($matches[1], true);
if (json_last_error() === JSON_ERROR_NONE && !empty($data)) {
$edges = $data['edges'] ?? array();
if (!empty($edges)) {
return $this->parse_graphql_edges($edges, $username);
}
}
}
}
return array();
}
/**
* Parse preloaded data from script tags
*
* @param string $html HTML content
* @param string $username Username
* @return array
*/
private function parse_preloaded_data($html, $username)
{
// Look for preloaded data
$patterns = array(
'/window\.__additionalDataLoaded\s*\(\s*[\'"][^\'"]+[\'"]\s*,\s*({.+?})\s*\)\s*;/s',
'/window\._sharedData\s*=\s*({.+?});/s',
'/<script type="application\/json"[^>]*>(\{.+?\})<\/script>/s',
);
foreach ($patterns as $pattern) {
if (preg_match_all($pattern, $html, $matches)) {
foreach ($matches[1] as $json_str) {
$data = json_decode($json_str, true);
if (json_last_error() !== JSON_ERROR_NONE) {
continue;
}
// Try to find edges in various paths
$edges = $this->find_edges_in_data($data);
if (!empty($edges)) {
return $this->parse_graphql_edges($edges, $username);
}
}
}
}
return array();
}
/**
* Find edges in nested data structure
*
* @param array $data Data array
* @return array
*/
private function find_edges_in_data($data)
{
if (!is_array($data)) {
return array();
}
// Direct path check
$paths = array(
array('entry_data', 'ProfilePage', 0, 'graphql', 'user', 'edge_owner_to_timeline_media', 'edges'),
array('graphql', 'user', 'edge_owner_to_timeline_media', 'edges'),
array('data', 'user', 'edge_owner_to_timeline_media', 'edges'),
array('user', 'edge_owner_to_timeline_media', 'edges'),
array('edge_owner_to_timeline_media', 'edges'),
array('require', 0, 3, 0, '__bbox', 'require', 0, 3, 1, '__bbox', 'result', 'data', 'xdt_api__v1__feed__user_timeline_graphql_connection', 'edges'),
);
foreach ($paths as $path) {
$result = $data;
foreach ($path as $key) {
if (is_array($result) && isset($result[$key])) {
$result = $result[$key];
} else {
$result = null;
break;
}
}
if (!empty($result) && is_array($result)) {
return $result;
}
}
// Recursive search for edges key
return $this->recursive_find_edges($data);
}
/**
* Recursively search for edges in data
*
* @param array $data Data array
* @param int $depth Current depth
* @return array
*/
private function recursive_find_edges($data, $depth = 0)
{
if ($depth > 10 || !is_array($data)) {
return array();
}
foreach ($data as $key => $value) {
if ($key === 'edges' && is_array($value) && !empty($value)) {
// Validate it looks like media edges
if (isset($value[0]['node']['display_url']) || isset($value[0]['node']['thumbnail_src'])) {
return $value;
}
}
if (is_array($value)) {
$result = $this->recursive_find_edges($value, $depth + 1);
if (!empty($result)) {
return $result;
}
}
}
return array();
}
/**
* Parse OG images from page as fallback
*
* @param string $html HTML content
* @param string $username Username
* @return array
*/
private function parse_og_images($html, $username)
{
$posts = array();
// Get OG image
if (preg_match('/property="og:image"[^>]+content="([^"]+)"/', $html, $matches)) {
$image_url = $matches[1];
// This is just the profile picture or first post, but it's something
$posts[] = array(
'instagram_id' => md5($username . '_og_' . time()),
'shortcode' => '',
'username' => $username,
'image_url' => $image_url,
'thumbnail_url' => $image_url,
'post_url' => 'https://www.instagram.com/' . $username . '/',
'caption' => '',
'likes_count' => null,
'comments_count' => null,
'posted_at' => current_time('mysql'),
'image_width' => 0,
'image_height' => 0,
'is_video' => false,
);
}
// Also try to find any image URLs that look like Instagram CDN
$pattern = '/(https:\/\/[^"\']+?instagram[^"\']+?\.jpg[^"\']*)/i';
if (preg_match_all($pattern, $html, $matches)) {
$seen = array();
foreach (array_slice(array_unique($matches[1]), 0, 12) as $idx => $img_url) {
// Skip profile pictures and very small images
if (strpos($img_url, '_a.jpg') !== false || strpos($img_url, '150x150') !== false) {
continue;
}
$hash = md5($img_url);
if (isset($seen[$hash])) {
continue;
}
$seen[$hash] = true;
$posts[] = array(
'instagram_id' => md5($username . '_img_' . $idx),
'shortcode' => '',
'username' => $username,
'image_url' => $img_url,
'thumbnail_url' => $img_url,
'post_url' => 'https://www.instagram.com/' . $username . '/',
'caption' => '',
'likes_count' => null,
'comments_count' => null,
'posted_at' => current_time('mysql'),
'image_width' => 0,
'image_height' => 0,
'is_video' => false,
);
}
}
return $posts;
}
/**
* Parse window._sharedData from HTML
*
* @param string $html HTML content
* @param string $username Username
* @return array
*/
private function parse_shared_data($html, $username)
{
$pattern = '/window\._sharedData\s*=\s*({.+?});/s';
if (!preg_match($pattern, $html, $matches)) {
return array();
}
$data = json_decode($matches[1], true);
if (json_last_error() !== JSON_ERROR_NONE) {
return array();
}
// Navigate to media edges
$edges = $data['entry_data']['ProfilePage'][0]['graphql']['user']['edge_owner_to_timeline_media']['edges'] ?? array();
if (empty($edges)) {
return array();
}
return $this->parse_graphql_edges($edges, $username);
}
/**
* Parse GraphQL edges into normalized post data
*
* @param array $edges GraphQL edges
* @param string $username Username
* @return array
*/
private function parse_graphql_edges($edges, $username)
{
$posts = array();
$max_images = (int) get_option('igsp_max_images', 12);
foreach ($edges as $index => $edge) {
if ($index >= $max_images) {
break;
}
$node = $edge['node'] ?? $edge;
if (empty($node)) {
continue;
}
$post = array(
'instagram_id' => $node['id'] ?? $node['pk'] ?? md5(json_encode($node)),
'shortcode' => $node['shortcode'] ?? $node['code'] ?? '',
'username' => $username,
'image_url' => $this->get_best_image_url($node),
'thumbnail_url' => $node['thumbnail_src'] ?? $node['display_url'] ?? $node['image_versions2']['candidates'][0]['url'] ?? '',
'post_url' => 'https://www.instagram.com/p/' . ($node['shortcode'] ?? $node['code'] ?? '') . '/',
'caption' => $this->extract_caption($node),
'likes_count' => $node['edge_liked_by']['count'] ?? $node['edge_media_preview_like']['count'] ?? $node['like_count'] ?? null,
'comments_count' => $node['edge_media_to_comment']['count'] ?? $node['edge_media_preview_comment']['count'] ?? $node['comment_count'] ?? null,
'posted_at' => isset($node['taken_at_timestamp']) ? date('Y-m-d H:i:s', $node['taken_at_timestamp']) : (isset($node['taken_at']) ? date('Y-m-d H:i:s', $node['taken_at']) : current_time('mysql')),
'image_width' => $node['dimensions']['width'] ?? $node['original_width'] ?? 0,
'image_height' => $node['dimensions']['height'] ?? $node['original_height'] ?? 0,
'is_video' => $node['is_video'] ?? $node['media_type'] === 2 ?? false,
);
// Skip videos if not supported
if (!empty($post['is_video'])) {
continue;
}
// Only add if we have an image
if (!empty($post['image_url'])) {
$posts[] = $post;
}
}
return $posts;
}
/**
* Get best image URL based on quality setting
*
* @param array $node Node data
* @return string
*/
private function get_best_image_url($node)
{
$quality = get_option('igsp_image_quality', 'high');
// Check for image_versions2 format (newer API)
if (!empty($node['image_versions2']['candidates'])) {
$candidates = $node['image_versions2']['candidates'];
switch ($quality) {
case 'thumbnail':
return end($candidates)['url'] ?? $candidates[0]['url'];
case 'medium':
$mid = floor(count($candidates) / 2);
return $candidates[$mid]['url'] ?? $candidates[0]['url'];
case 'high':
default:
return $candidates[0]['url'];
}
}
switch ($quality) {
case 'thumbnail':
return $node['thumbnail_src'] ?? $node['thumbnail_resources'][0]['src'] ?? $node['display_url'] ?? '';
case 'medium':
$resources = $node['thumbnail_resources'] ?? array();
$mid_index = (int) floor(count($resources) / 2);
return $resources[$mid_index]['src'] ?? $node['display_url'] ?? '';
case 'high':
default:
return $node['display_url'] ?? $node['thumbnail_src'] ?? '';
}
}
/**
* Extract caption from node
*
* @param array $node Node data
* @return string
*/
private function extract_caption($node)
{
$caption = '';
if (isset($node['edge_media_to_caption']['edges'][0]['node']['text'])) {
$caption = $node['edge_media_to_caption']['edges'][0]['node']['text'];
} elseif (isset($node['caption']['text'])) {
$caption = $node['caption']['text'];
} elseif (isset($node['caption']) && is_string($node['caption'])) {
$caption = $node['caption'];
}
return wp_kses_post($caption);
}
/**
* Make HTTP request
*
* @param string $url URL to request
* @param array $args Additional arguments
* @return array|WP_Error
*/
private function make_request($url, $args = array())
{
$custom_ua = get_option('igsp_user_agent', '');
$user_agent = !empty($custom_ua) ? $custom_ua : $this->get_random_user_agent();
$default_args = array(
'timeout' => $this->timeout,
'user-agent' => $user_agent,
'headers' => array(
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.9',
'Accept-Encoding' => 'gzip, deflate, br',
'Connection' => 'keep-alive',
'Upgrade-Insecure-Requests' => '1',
'Sec-Fetch-Dest' => 'document',
'Sec-Fetch-Mode' => 'navigate',
'Sec-Fetch-Site' => 'none',
'Sec-Fetch-User' => '?1',
'Sec-Ch-Ua' => '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile' => '?0',
'Sec-Ch-Ua-Platform' => '"Windows"',
'Cache-Control' => 'max-age=0',
),
'sslverify' => true,
'cookies' => array(),
);
// Merge custom headers
if (isset($args['headers'])) {
$args['headers'] = array_merge($default_args['headers'], $args['headers']);
}
$args = wp_parse_args($args, $default_args);
// Add proxy if configured
$proxy_host = get_option('igsp_proxy_host', '');
$proxy_port = get_option('igsp_proxy_port', '');
if (!empty($proxy_host) && !empty($proxy_port)) {
$args['proxy'] = $proxy_host . ':' . $proxy_port;
}
// Retry logic
$last_error = null;
for ($attempt = 1; $attempt <= $this->max_retries; $attempt++) {
$response = wp_remote_get($url, $args);
if (!is_wp_error($response)) {
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code === 200) {
return $response;
}
if ($status_code === 429) {
// Rate limited - wait longer
$this->logger->warning(__('Rate limited by Instagram, waiting...', 'instagram-gallery-sync-pro'));
sleep(30);
} elseif ($status_code === 302 || $status_code === 301) {
// Follow redirect
$location = wp_remote_retrieve_header($response, 'location');
if (!empty($location)) {
$this->random_delay(1, 2);
return $this->make_request($location, $args);
}
} elseif ($status_code >= 400) {
$last_error = new WP_Error(
'http_error',
sprintf(__('HTTP Error: %d', 'instagram-gallery-sync-pro'), $status_code)
);
}
} else {
$last_error = $response;
}
if ($attempt < $this->max_retries) {
$this->random_delay($this->min_delay * $attempt, $this->max_delay * $attempt);
}
}
return $last_error ?? new WP_Error('request_failed', __('Request failed after retries.', 'instagram-gallery-sync-pro'));
}
/**
* Get random User-Agent
*
* @return string
*/
private function get_random_user_agent()
{
return $this->user_agents[array_rand($this->user_agents)];
}
/**
* Random delay between requests
*
* @param int $min Minimum seconds
* @param int $max Maximum seconds
* @return void
*/
private function random_delay($min = null, $max = null)
{
$min = $min ?? $this->min_delay;
$max = $max ?? $this->max_delay;
$delay = rand($min * 1000000, $max * 1000000); // Microseconds
usleep($delay);
}
/**
* Validate fetched data
*
* @param array $data Post data
* @return bool
*/
public function validate_post_data($data)
{
if (empty($data['instagram_id'])) {
return false;
}
if (empty($data['image_url'])) {
return false;
}
// Validate URL format
if (!filter_var($data['image_url'], FILTER_VALIDATE_URL)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,532 @@
<?php
/**
* Shortcode Handler Class
*
* Handles the [instagram_gallery] shortcode rendering.
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Class IGSP_Shortcode
*/
class IGSP_Shortcode
{
/**
* Default shortcode attributes
*
* @var array
*/
private $defaults;
/**
* Initialize shortcode
*
* @return void
*/
public function init()
{
add_shortcode('instagram_gallery', array($this, 'render'));
add_action('wp_enqueue_scripts', array($this, 'register_assets'));
}
/**
* Get default attributes
*
* @return array
*/
private function get_defaults()
{
if (!$this->defaults) {
$this->defaults = array(
'layout' => get_option('igsp_layout_type', 'grid'),
'columns' => get_option('igsp_columns_desktop', 3),
'spacing' => get_option('igsp_spacing', 10),
'limit' => get_option('igsp_display_limit', 12),
'order' => get_option('igsp_order', 'newest'),
'lightbox' => get_option('igsp_lightbox', 'yes'),
'captions' => get_option('igsp_show_caption', 'no'),
'autoplay' => 'false',
'class' => '',
);
}
return $this->defaults;
}
/**
* Register frontend assets
*
* @return void
*/
public function register_assets()
{
$prefix = get_option('igsp_css_prefix', 'igsp');
// Base styles
wp_register_style(
'igsp-gallery-base',
IGSP_PLUGIN_URL . 'public/css/gallery-base.css',
array(),
IGSP_VERSION
);
// Layout-specific styles
wp_register_style(
'igsp-gallery-masonry',
IGSP_PLUGIN_URL . 'public/css/gallery-masonry.css',
array('igsp-gallery-base'),
IGSP_VERSION
);
wp_register_style(
'igsp-gallery-grid',
IGSP_PLUGIN_URL . 'public/css/gallery-grid.css',
array('igsp-gallery-base'),
IGSP_VERSION
);
wp_register_style(
'igsp-gallery-slider',
IGSP_PLUGIN_URL . 'public/css/gallery-slider.css',
array('igsp-gallery-base'),
IGSP_VERSION
);
// Lightbox
wp_register_style(
'igsp-lightbox',
IGSP_PLUGIN_URL . 'public/css/lightbox.css',
array(),
IGSP_VERSION
);
// Scripts
wp_register_script(
'igsp-masonry',
IGSP_PLUGIN_URL . 'public/js/masonry.pkgd.min.js',
array(),
'4.2.2',
true
);
wp_register_script(
'igsp-imagesloaded',
IGSP_PLUGIN_URL . 'public/js/imagesloaded.pkgd.min.js',
array(),
'5.0.0',
true
);
wp_register_script(
'igsp-glightbox',
IGSP_PLUGIN_URL . 'public/js/glightbox.min.js',
array(),
'3.2.0',
true
);
wp_register_script(
'igsp-swiper',
IGSP_PLUGIN_URL . 'public/js/swiper.min.js',
array(),
'11.0.5',
true
);
wp_register_script(
'igsp-gallery-frontend',
IGSP_PLUGIN_URL . 'public/js/gallery-frontend.js',
array('jquery'),
IGSP_VERSION,
true
);
}
/**
* Render shortcode
*
* @param array $atts Shortcode attributes
* @return string
*/
public function render($atts = array())
{
$atts = shortcode_atts($this->get_defaults(), $atts, 'instagram_gallery');
// Sanitize attributes
$atts = $this->sanitize_atts($atts);
// Check cache
$cache_key = 'igsp_gallery_' . md5(serialize($atts));
$cache_duration = (int) get_option('igsp_cache_duration', 3600);
$cached = get_transient($cache_key);
if ($cached !== false && !is_admin()) {
$this->enqueue_assets($atts);
return $cached;
}
// Get posts from database
$database = new IGSP_Database();
$posts = $database->get_posts(array(
'limit' => $atts['limit'],
'order' => $atts['order'],
));
if (empty($posts)) {
return $this->render_empty_state();
}
// Enqueue assets
$this->enqueue_assets($atts);
// Render gallery
$output = $this->render_gallery($posts, $atts);
// Cache output
set_transient($cache_key, $output, $cache_duration);
return $output;
}
/**
* Sanitize attributes
*
* @param array $atts Attributes
* @return array
*/
private function sanitize_atts($atts)
{
$atts['layout'] = sanitize_key($atts['layout']);
$atts['columns'] = absint($atts['columns']);
$atts['spacing'] = absint($atts['spacing']);
$atts['limit'] = absint($atts['limit']);
$atts['order'] = sanitize_key($atts['order']);
$atts['lightbox'] = in_array($atts['lightbox'], array('yes', 'true', '1'), true) ? true : false;
$atts['captions'] = in_array($atts['captions'], array('yes', 'true', '1'), true) ? true : false;
$atts['autoplay'] = in_array($atts['autoplay'], array('yes', 'true', '1'), true) ? true : false;
$atts['class'] = sanitize_html_class($atts['class']);
// Clamp values
$atts['columns'] = max(1, min(6, $atts['columns']));
$atts['spacing'] = max(0, min(50, $atts['spacing']));
$atts['limit'] = max(1, min(100, $atts['limit']));
return $atts;
}
/**
* Enqueue required assets
*
* @param array $atts Attributes
* @return void
*/
private function enqueue_assets($atts)
{
// Base CSS
wp_enqueue_style('igsp-gallery-base');
// Layout-specific CSS
switch ($atts['layout']) {
case 'masonry':
wp_enqueue_style('igsp-gallery-masonry');
wp_enqueue_script('igsp-masonry');
wp_enqueue_script('igsp-imagesloaded');
break;
case 'slider':
wp_enqueue_style('igsp-gallery-slider');
wp_enqueue_script('igsp-swiper');
break;
default:
wp_enqueue_style('igsp-gallery-grid');
}
// Lightbox
if ($atts['lightbox']) {
wp_enqueue_style('igsp-lightbox');
wp_enqueue_script('igsp-glightbox');
}
// Frontend JS
wp_enqueue_script('igsp-gallery-frontend');
// Pass settings to JS
wp_localize_script('igsp-gallery-frontend', 'igspFrontend', array(
'lightbox' => $atts['lightbox'],
'layout' => $atts['layout'],
'autoplay' => $atts['autoplay'],
));
// Add inline custom CSS
$this->add_custom_styles();
}
/**
* Add custom inline styles
*
* @return void
*/
private function add_custom_styles()
{
$primary = get_option('igsp_primary_color', '#e1306c');
$hover = get_option('igsp_hover_color', '#c13584');
$text = get_option('igsp_text_color', '#ffffff');
$font_size = get_option('igsp_caption_font_size', 14);
$border_radius = get_option('igsp_border_radius', 0);
$custom_css = get_option('igsp_custom_css', '');
$css = "
:root {
--igsp-primary: {$primary};
--igsp-hover: {$hover};
--igsp-text: {$text};
--igsp-font-size: {$font_size}px;
--igsp-radius: {$border_radius}px;
}
";
if (!empty($custom_css)) {
$css .= "\n" . $custom_css;
}
wp_add_inline_style('igsp-gallery-base', $css);
}
/**
* Render gallery HTML
*
* @param array $posts Posts
* @param array $atts Attributes
* @return string
*/
private function render_gallery($posts, $atts)
{
$prefix = get_option('igsp_css_prefix', 'igsp');
$hover_effect = get_option('igsp_hover_effect', 'zoom');
$aspect_ratio = get_option('igsp_aspect_ratio', 'square');
$object_fit = get_option('igsp_object_fit', 'cover');
$caption_position = get_option('igsp_caption_position', 'overlay');
$link_behavior = get_option('igsp_link_behavior', 'new_tab');
$lazy_loading = get_option('igsp_lazy_loading', 'yes') === 'yes';
$columns_tablet = get_option('igsp_columns_tablet', 2);
$columns_mobile = get_option('igsp_columns_mobile', 1);
$wrapper_class = array(
$prefix . '-gallery',
$prefix . '-' . $atts['layout'],
$prefix . '-hover-' . $hover_effect,
$prefix . '-aspect-' . $aspect_ratio,
);
if (!empty($atts['class'])) {
$wrapper_class[] = $atts['class'];
}
// Build gallery container
ob_start();
?>
<div class="<?php echo esc_attr(implode(' ', $wrapper_class)); ?>"
data-columns="<?php echo esc_attr($atts['columns']); ?>"
data-columns-tablet="<?php echo esc_attr($columns_tablet); ?>"
data-columns-mobile="<?php echo esc_attr($columns_mobile); ?>"
data-spacing="<?php echo esc_attr($atts['spacing']); ?>" data-layout="<?php echo esc_attr($atts['layout']); ?>"
style="--columns: <?php echo esc_attr($atts['columns']); ?>; --spacing: <?php echo esc_attr($atts['spacing']); ?>px;">
<?php if ($atts['layout'] === 'slider'): ?>
<div class="swiper">
<div class="swiper-wrapper">
<?php endif; ?>
<?php foreach ($posts as $post): ?>
<?php echo $this->render_item($post, $atts, $lazy_loading, $caption_position, $link_behavior); ?>
<?php endforeach; ?>
<?php if ($atts['layout'] === 'slider'): ?>
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<?php endif; ?>
</div>
<?php if (get_option('igsp_show_instagram_btn', 'yes') === 'yes'): ?>
<?php echo $this->render_instagram_button(); ?>
<?php endif; ?>
<?php
return ob_get_clean();
}
/**
* Render single gallery item
*
* @param object $post Post object
* @param array $atts Attributes
* @param bool $lazy_loading Use lazy loading
* @param string $caption_position Caption position
* @param string $link_behavior Link behavior
* @return string
*/
private function render_item($post, $atts, $lazy_loading, $caption_position, $link_behavior)
{
$prefix = get_option('igsp_css_prefix', 'igsp');
$image_handler = new IGSP_Image_Handler();
// Get image URL
$image_url = '';
if (!empty($post->image_local_path)) {
$image_url = $image_handler->get_image_url($post->image_local_path);
}
$thumb_url = '';
if (!empty($post->image_thumbnail_path)) {
$thumb_url = $image_handler->get_image_url($post->image_thumbnail_path);
}
// Fallback to main image if no thumbnail
$display_url = !empty($thumb_url) ? $thumb_url : $image_url;
$full_url = $image_url;
if (empty($display_url)) {
return '';
}
// Determine link
$link_url = '';
$link_target = '';
$lightbox_attr = '';
if ($link_behavior === 'lightbox' && $atts['lightbox']) {
$link_url = $full_url;
$lightbox_attr = 'data-gallery="igsp-lightbox"';
} elseif ($link_behavior === 'new_tab') {
$link_url = $post->post_url;
$link_target = 'target="_blank" rel="noopener noreferrer"';
} elseif ($link_behavior === 'same_window') {
$link_url = $post->post_url;
}
// Caption
$caption = '';
if ($atts['captions'] && !empty($post->caption)) {
$caption = wp_trim_words($post->caption, 20);
}
// Build class based on layout
$item_class = $prefix . '-item';
if ($atts['layout'] === 'slider') {
$item_class = 'swiper-slide ' . $item_class;
}
ob_start();
?>
<div class="<?php echo esc_attr($item_class); ?>">
<?php if (!empty($link_url)): ?>
<a href="<?php echo esc_url($link_url); ?>" class="<?php echo esc_attr($prefix . '-link'); ?>" <?php echo $link_target; ?>
<?php echo $lightbox_attr; ?>
title="
<?php echo esc_attr($caption); ?>">
<?php endif; ?>
<div class="<?php echo esc_attr($prefix . '-image-wrapper'); ?>">
<img src="<?php echo $lazy_loading ? '' : esc_url($display_url); ?>" <?php echo $lazy_loading ? 'data-src="' . esc_url($display_url) . '"' : ''; ?>
class="
<?php echo esc_attr($prefix . '-image'); ?>
<?php echo $lazy_loading ? 'lazyload' : ''; ?>"
alt="
<?php echo esc_attr($caption); ?>"
<?php echo $lazy_loading ? 'loading="lazy"' : ''; ?>>
<?php if ($caption_position === 'overlay' && $atts['captions'] && !empty($caption)): ?>
<div class="<?php echo esc_attr($prefix . '-overlay'); ?>">
<span class="<?php echo esc_attr($prefix . '-caption'); ?>">
<?php echo esc_html($caption); ?>
</span>
</div>
<?php endif; ?>
<div class="<?php echo esc_attr($prefix . '-hover-overlay'); ?>">
<span class="<?php echo esc_attr($prefix . '-icon'); ?>">
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm0 7.082c1.602 0 1.792.006 2.425.035.598.028.922.127 1.138.211.286.111.491.244.705.458s.347.419.458.705c.084.216.183.54.211 1.138.029.633.035.823.035 2.425s-.006 1.792-.035 2.425c-.028.598-.127.922-.211 1.138-.111.286-.244.491-.458.705s-.419.347-.705.458c-.216.084-.54.183-1.138.211-.633.029-.823.035-2.425.035s-1.792-.006-2.425-.035c-.598-.028-.922-.127-1.138-.211-.286-.111-.491-.244-.705-.458s-.347-.419-.458-.705c-.084-.216-.183-.54-.211-1.138-.029-.633-.035-.823-.035-2.425s.006-1.792.035-2.425c.028-.598.127-.922.211-1.138.111-.286.244-.491.458-.705s.419-.347.705-.458c.216-.084.54-.183 1.138-.211.633-.029.823-.035 2.425-.035zm0-1.082c-1.63 0-1.833.007-2.474.036-.639.029-1.076.131-1.459.28-.396.154-.731.359-1.066.693s-.539.67-.693 1.066c-.149.383-.251.82-.28 1.459-.029.641-.036.844-.036 2.474s.007 1.833.036 2.474c.029.639.131 1.076.28 1.459.154.396.359.731.693 1.066s.67.539 1.066.693c.383.149.82.251 1.459.28.641.029.844.036 2.474.036s1.833-.007 2.474-.036c.639-.029 1.076-.131 1.459-.28.396-.154.731-.359 1.066-.693s.539-.67.693-1.066c.149-.383.251-.82.28-1.459.029-.641.036-.844.036-2.474s-.007-1.833-.036-2.474c-.029-.639-.131-1.076-.28-1.459-.154-.396-.359-.731-.693-1.066s-.67-.539-1.066-.693c-.383-.149-.82-.251-1.459-.28-.641-.029-.844-.036-2.474-.036zm0 2.919c-1.701 0-3.081 1.379-3.081 3.081s1.379 3.081 3.081 3.081 3.081-1.379 3.081-3.081-1.379-3.081-3.081-3.081zm0 5.081c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm3.202-5.922c-.398 0-.72.322-.72.72s.322.72.72.72.72-.322.72-.72-.322-.72-.72-.72z" />
</svg>
</span>
</div>
</div>
<?php if (!empty($link_url)): ?>
</a>
<?php endif; ?>
<?php if ($caption_position === 'below' && $atts['captions'] && !empty($caption)): ?>
<div class="<?php echo esc_attr($prefix . '-caption-below'); ?>">
<?php echo esc_html($caption); ?>
</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Render Instagram button
*
* @return string
*/
private function render_instagram_button()
{
$prefix = get_option('igsp_css_prefix', 'igsp');
$username = get_option('igsp_username', '');
if (empty($username)) {
return '';
}
ob_start();
?>
<div class="<?php echo esc_attr($prefix . '-follow-btn-wrapper'); ?>">
<a href="https://www.instagram.com/<?php echo esc_attr($username); ?>/" target="_blank" rel="noopener noreferrer"
class="<?php echo esc_attr($prefix . '-follow-btn'); ?>">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<path
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
</svg>
<?php esc_html_e('Follow on Instagram', 'instagram-gallery-sync-pro'); ?>
</a>
</div>
<?php
return ob_get_clean();
}
/**
* Render empty state
*
* @return string
*/
private function render_empty_state()
{
$prefix = get_option('igsp_css_prefix', 'igsp');
ob_start();
?>
<div class="<?php echo esc_attr($prefix . '-empty'); ?>">
<p>
<?php esc_html_e('No Instagram posts found. Please sync your Instagram account in the admin settings.', 'instagram-gallery-sync-pro'); ?>
</p>
</div>
<?php
return ob_get_clean();
}
}

1
includes/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden

View File

@@ -0,0 +1,369 @@
<?php
/**
* Plugin Name: Instagram Gallery Sync Pro
* Plugin URI: https://example.com/instagram-gallery-sync-pro
* Description: Synchronize and display Instagram photos in beautiful galleries without the official API.
* Version: 1.0.0
* Author: Your Name
* Author URI: https://example.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: instagram-gallery-sync-pro
* Domain Path: /languages
* Requires at least: 5.8
* Requires PHP: 7.4
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin Constants
*/
define( 'IGSP_VERSION', '1.0.0' );
define( 'IGSP_PLUGIN_FILE', __FILE__ );
define( 'IGSP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'IGSP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'IGSP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
define( 'IGSP_UPLOAD_DIR', 'instagram-gallery' );
/**
* Database Table Names
*/
global $wpdb;
define( 'IGSP_TABLE_POSTS', $wpdb->prefix . 'instagram_gallery_posts' );
define( 'IGSP_TABLE_LOG', $wpdb->prefix . 'instagram_gallery_log' );
/**
* Autoloader for plugin classes
*/
spl_autoload_register( function( $class ) {
// Check if the class uses our namespace prefix
$prefix = 'IGSP_';
if ( strpos( $class, $prefix ) !== 0 ) {
return;
}
// Convert class name to file name
$class_name = str_replace( $prefix, '', $class );
$class_name = strtolower( str_replace( '_', '-', $class_name ) );
$file = IGSP_PLUGIN_DIR . 'includes/class-' . $class_name . '.php';
if ( file_exists( $file ) ) {
require_once $file;
}
});
/**
* Main Plugin Class
*/
final class Instagram_Gallery_Sync_Pro {
/**
* Plugin instance
*
* @var Instagram_Gallery_Sync_Pro
*/
private static $instance = null;
/**
* Plugin components
*/
public $database;
public $logger;
public $scraper;
public $image_handler;
public $cron;
public $admin;
public $shortcode;
public $gutenberg;
/**
* Get plugin instance
*
* @return Instagram_Gallery_Sync_Pro
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Activation and deactivation hooks
register_activation_hook( IGSP_PLUGIN_FILE, array( $this, 'activate' ) );
register_deactivation_hook( IGSP_PLUGIN_FILE, array( $this, 'deactivate' ) );
// Initialize plugin after WordPress is loaded
add_action( 'plugins_loaded', array( $this, 'init_plugin' ) );
// Load text domain
add_action( 'init', array( $this, 'load_textdomain' ) );
}
/**
* Plugin activation
*/
public function activate() {
// Ensure components are loaded
$this->load_components();
// Create database tables
$this->database->create_tables();
// Create upload directory
$this->create_upload_directory();
// Set default options
$this->set_default_options();
// Schedule cron if enabled
$this->cron->schedule_sync();
// Log activation
$this->logger->log( 'info', __( 'Plugin activated successfully.', 'instagram-gallery-sync-pro' ) );
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Clear scheduled cron
$this->cron->unschedule_sync();
// Clear transients
$this->clear_transients();
// Log deactivation
$this->logger->log( 'info', __( 'Plugin deactivated.', 'instagram-gallery-sync-pro' ) );
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Initialize plugin components
*/
public function init_plugin() {
$this->load_components();
$this->init_components();
}
/**
* Load plugin components
*/
private function load_components() {
// Core components
$this->database = new IGSP_Database();
$this->logger = new IGSP_Logger();
$this->image_handler = new IGSP_Image_Handler();
$this->scraper = new IGSP_Scraper();
$this->cron = new IGSP_Cron();
// Frontend components
$this->shortcode = new IGSP_Shortcode();
$this->gutenberg = new IGSP_Gutenberg_Block();
// Admin components (only in admin)
if ( is_admin() ) {
$this->admin = new IGSP_Admin();
}
}
/**
* Initialize components
*/
private function init_components() {
$this->shortcode->init();
$this->gutenberg->init();
$this->cron->init();
if ( is_admin() && $this->admin ) {
$this->admin->init();
}
}
/**
* Load plugin text domain
*/
public function load_textdomain() {
load_plugin_textdomain(
'instagram-gallery-sync-pro',
false,
dirname( IGSP_PLUGIN_BASENAME ) . '/languages'
);
}
/**
* Create upload directory
*/
private function create_upload_directory() {
$upload_dir = wp_upload_dir();
$instagram_dir = $upload_dir['basedir'] . '/' . IGSP_UPLOAD_DIR;
if ( ! file_exists( $instagram_dir ) ) {
wp_mkdir_p( $instagram_dir );
// Create index.php for security
file_put_contents( $instagram_dir . '/index.php', '<?php // Silence is golden' );
// Create .htaccess for additional security
$htaccess = "Options -Indexes\n";
$htaccess .= "<Files *.php>\n";
$htaccess .= "deny from all\n";
$htaccess .= "</Files>\n";
file_put_contents( $instagram_dir . '/.htaccess', $htaccess );
}
// Create thumbnails subfolder
$thumb_dir = $instagram_dir . '/thumbnails';
if ( ! file_exists( $thumb_dir ) ) {
wp_mkdir_p( $thumb_dir );
file_put_contents( $thumb_dir . '/index.php', '<?php // Silence is golden' );
}
}
/**
* Set default options
*/
private function set_default_options() {
$defaults = array(
// Instagram settings
'igsp_username' => '',
'igsp_max_images' => 12,
'igsp_save_locally' => 'yes',
'igsp_image_quality' => 'high',
'igsp_sync_interval' => 'daily',
'igsp_last_sync' => '',
// Layout settings
'igsp_layout_type' => 'grid',
'igsp_columns_desktop' => 3,
'igsp_columns_tablet' => 2,
'igsp_columns_mobile' => 1,
'igsp_spacing' => 10,
'igsp_padding' => 0,
'igsp_aspect_ratio' => 'square',
'igsp_object_fit' => 'cover',
'igsp_border_radius' => 0,
'igsp_hover_effect' => 'zoom',
// Display settings
'igsp_display_limit' => 12,
'igsp_order' => 'newest',
'igsp_show_caption' => 'no',
'igsp_caption_position' => 'overlay',
'igsp_show_instagram_btn' => 'yes',
'igsp_link_behavior' => 'new_tab',
'igsp_lightbox' => 'yes',
'igsp_lazy_loading' => 'yes',
'igsp_loader_type' => 'spinner',
// Styling
'igsp_primary_color' => '#e1306c',
'igsp_hover_color' => '#c13584',
'igsp_text_color' => '#ffffff',
'igsp_caption_font_size' => 14,
'igsp_custom_css' => '',
'igsp_css_prefix' => 'igsp',
// Advanced
'igsp_debug_mode' => 'no',
'igsp_cache_duration' => 3600,
'igsp_request_timeout' => 30,
'igsp_user_agent' => '',
'igsp_proxy_host' => '',
'igsp_proxy_port' => '',
'igsp_auto_delete_days' => 0,
);
foreach ( $defaults as $key => $value ) {
if ( get_option( $key ) === false ) {
add_option( $key, $value );
}
}
}
/**
* Clear all transients
*/
public function clear_transients() {
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_igsp_%'
OR option_name LIKE '_transient_timeout_igsp_%'"
);
}
/**
* Get upload directory path
*
* @return string
*/
public function get_upload_path() {
$upload_dir = wp_upload_dir();
return $upload_dir['basedir'] . '/' . IGSP_UPLOAD_DIR;
}
/**
* Get upload directory URL
*
* @return string
*/
public function get_upload_url() {
$upload_dir = wp_upload_dir();
return $upload_dir['baseurl'] . '/' . IGSP_UPLOAD_DIR;
}
/**
* Get plugin settings
*
* @param string $key Option key
* @param mixed $default Default value
* @return mixed
*/
public function get_setting( $key, $default = '' ) {
return get_option( 'igsp_' . $key, $default );
}
}
/**
* Returns the main plugin instance
*
* @return Instagram_Gallery_Sync_Pro
*/
function igsp() {
return Instagram_Gallery_Sync_Pro::get_instance();
}
/**
* Template function for displaying the gallery
*
* @param array $args Gallery arguments
* @return void
*/
function display_instagram_gallery( $args = array() ) {
echo igsp()->shortcode->render( $args );
}
// Initialize the plugin
igsp();

View File

@@ -0,0 +1,585 @@
# Copyright (C) 2024 Instagram Gallery Sync Pro
# This file is distributed under the GPL-2.0+.
msgid ""
msgstr ""
"Project-Id-Version: Instagram Gallery Sync Pro 1.0.0\n"
"Report-Msgid-Bugs-To: https://example.com\n"
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
#: includes/class-admin.php
msgid "Instagram Gallery"
msgstr ""
#: includes/class-admin.php
msgid "Settings"
msgstr ""
#: admin/views/settings-page.php
msgid "Instagram Gallery Sync Pro"
msgstr ""
#: admin/views/settings-page.php
msgid "Version %s"
msgstr ""
#: admin/views/settings-page.php
msgid "Sync Status"
msgstr ""
#: admin/views/settings-page.php
msgid "Total Posts:"
msgstr ""
#: admin/views/settings-page.php
msgid "Last Sync:"
msgstr ""
#: admin/views/settings-page.php
msgid "Next Sync:"
msgstr ""
#: admin/views/settings-page.php
msgid "Shortcode"
msgstr ""
#: admin/views/settings-page.php
msgid "Use this shortcode to display the gallery on any page or post."
msgstr ""
#: admin/views/settings-page.php
msgid "Need Help?"
msgstr ""
#: admin/views/settings-page.php
msgid "Documentation"
msgstr ""
#: admin/views/settings-page.php
msgid "Support"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Account Settings"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Instagram Username"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Enter the Instagram username without the @ symbol. The profile must be public."
msgstr ""
#: admin/views/tab-instagram.php
msgid "Maximum Images to Fetch"
msgstr ""
#: admin/views/tab-instagram.php
msgid "All available"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Number of images to fetch from Instagram."
msgstr ""
#: admin/views/tab-instagram.php
msgid "Save Images Locally"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Yes - Download and store images on your server (recommended)"
msgstr ""
#: admin/views/tab-instagram.php
msgid "No - Link directly to Instagram (may cause loading issues)"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Image Quality"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Thumbnail (150px)"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Medium (320px)"
msgstr ""
#: admin/views/tab-instagram.php
msgid "High (640px+)"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Synchronization"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Sync Interval"
msgstr ""
#: admin/views/tab-instagram.php
msgid "How often to check for new posts."
msgstr ""
#: admin/views/tab-instagram.php
msgid "Manual Sync"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Sync Now"
msgstr ""
#: admin/views/tab-instagram.php
msgid "Syncing..."
msgstr ""
#: admin/views/tab-layout.php
msgid "Layout Type"
msgstr ""
#: admin/views/tab-layout.php
msgid "Grid"
msgstr ""
#: admin/views/tab-layout.php
msgid "Masonry"
msgstr ""
#: admin/views/tab-layout.php
msgid "Justified"
msgstr ""
#: admin/views/tab-layout.php
msgid "Slider"
msgstr ""
#: admin/views/tab-layout.php
msgid "List"
msgstr ""
#: admin/views/tab-layout.php
msgid "Grid Settings"
msgstr ""
#: admin/views/tab-layout.php
msgid "Columns (Desktop)"
msgstr ""
#: admin/views/tab-layout.php
msgid "Columns (Tablet)"
msgstr ""
#: admin/views/tab-layout.php
msgid "Columns (Mobile)"
msgstr ""
#: admin/views/tab-layout.php
msgid "Spacing"
msgstr ""
#: admin/views/tab-layout.php
msgid "Outer Padding"
msgstr ""
#: admin/views/tab-layout.php
msgid "Image Settings"
msgstr ""
#: admin/views/tab-layout.php
msgid "Aspect Ratio"
msgstr ""
#: admin/views/tab-layout.php
msgid "Original"
msgstr ""
#: admin/views/tab-layout.php
msgid "Square (1:1)"
msgstr ""
#: admin/views/tab-layout.php
msgid "Object Fit"
msgstr ""
#: admin/views/tab-layout.php
msgid "Cover (Crop to fill)"
msgstr ""
#: admin/views/tab-layout.php
msgid "Contain (Fit inside)"
msgstr ""
#: admin/views/tab-layout.php
msgid "Border Radius"
msgstr ""
#: admin/views/tab-layout.php
msgid "Hover Effect"
msgstr ""
#: admin/views/tab-layout.php
msgid "None"
msgstr ""
#: admin/views/tab-layout.php
msgid "Zoom"
msgstr ""
#: admin/views/tab-layout.php
msgid "Fade + Caption"
msgstr ""
#: admin/views/tab-layout.php
msgid "Overlay with Icon"
msgstr ""
#: admin/views/tab-layout.php
msgid "Grayscale to Color"
msgstr ""
#: admin/views/tab-layout.php
msgid "Lift (Shadow + Transform)"
msgstr ""
#: admin/views/tab-display.php
msgid "Display Options"
msgstr ""
#: admin/views/tab-display.php
msgid "Number of Images to Display"
msgstr ""
#: admin/views/tab-display.php
msgid "Display Order"
msgstr ""
#: admin/views/tab-display.php
msgid "Newest First"
msgstr ""
#: admin/views/tab-display.php
msgid "Oldest First"
msgstr ""
#: admin/views/tab-display.php
msgid "Random"
msgstr ""
#: admin/views/tab-display.php
msgid "Show Captions"
msgstr ""
#: admin/views/tab-display.php
msgid "Yes"
msgstr ""
#: admin/views/tab-display.php
msgid "No"
msgstr ""
#: admin/views/tab-display.php
msgid "Caption Position"
msgstr ""
#: admin/views/tab-display.php
msgid "Overlay on Image"
msgstr ""
#: admin/views/tab-display.php
msgid "Below Image"
msgstr ""
#: admin/views/tab-display.php
msgid "Show \"More on Instagram\" Button"
msgstr ""
#: admin/views/tab-display.php
msgid "Interactivity"
msgstr ""
#: admin/views/tab-display.php
msgid "Link Behavior"
msgstr ""
#: admin/views/tab-display.php
msgid "Open in New Tab"
msgstr ""
#: admin/views/tab-display.php
msgid "Open in Same Window"
msgstr ""
#: admin/views/tab-display.php
msgid "Open in Lightbox"
msgstr ""
#: admin/views/tab-display.php
msgid "No Link"
msgstr ""
#: admin/views/tab-display.php
msgid "Enable Lightbox"
msgstr ""
#: admin/views/tab-display.php
msgid "Allow clicking images to view them in a lightbox."
msgstr ""
#: admin/views/tab-display.php
msgid "Performance"
msgstr ""
#: admin/views/tab-display.php
msgid "Lazy Loading"
msgstr ""
#: admin/views/tab-display.php
msgid "Yes (Recommended)"
msgstr ""
#: admin/views/tab-display.php
msgid "Loading Animation"
msgstr ""
#: admin/views/tab-display.php
msgid "Spinner"
msgstr ""
#: admin/views/tab-display.php
msgid "Dots"
msgstr ""
#: admin/views/tab-display.php
msgid "Pulse"
msgstr ""
#: admin/views/tab-display.php
msgid "Skeleton"
msgstr ""
#: admin/views/tab-styling.php
msgid "Colors"
msgstr ""
#: admin/views/tab-styling.php
msgid "Primary Color"
msgstr ""
#: admin/views/tab-styling.php
msgid "Used for buttons and highlights."
msgstr ""
#: admin/views/tab-styling.php
msgid "Hover Color"
msgstr ""
#: admin/views/tab-styling.php
msgid "Used for overlay and hover effects."
msgstr ""
#: admin/views/tab-styling.php
msgid "Text Color"
msgstr ""
#: admin/views/tab-styling.php
msgid "Used for caption and overlay text."
msgstr ""
#: admin/views/tab-styling.php
msgid "Typography"
msgstr ""
#: admin/views/tab-styling.php
msgid "Caption Font Size"
msgstr ""
#: admin/views/tab-styling.php
msgid "Advanced Styling"
msgstr ""
#: admin/views/tab-styling.php
msgid "CSS Class Prefix"
msgstr ""
#: admin/views/tab-styling.php
msgid "Custom CSS"
msgstr ""
#: admin/views/tab-styling.php
msgid "Add custom CSS to override default styles. Use cautiously."
msgstr ""
#: admin/views/tab-styling.php
msgid "Live Preview"
msgstr ""
#: admin/views/tab-styling.php
msgid "Sample Caption"
msgstr ""
#: admin/views/tab-styling.php
msgid "Sample Button"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Debug & Development"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Debug Mode"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Enabled"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Disabled"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Enable to log all sync activities. Disable in production for better performance."
msgstr ""
#: admin/views/tab-advanced.php
msgid "Cache Duration"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Request Timeout"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Auto-Delete Old Posts"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Never"
msgstr ""
#: admin/views/tab-advanced.php
msgid "days"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Request Configuration"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Custom User-Agent"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Proxy Settings"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Tools"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Clear Cache"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Clear all cached gallery data."
msgstr ""
#: admin/views/tab-advanced.php
msgid "Storage Used"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Total: %s"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Reset All Data"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Delete ALL Instagram data, images, and logs."
msgstr ""
#: admin/views/tab-advanced.php
msgid "Reset Everything"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Activity Logs"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Clear Logs"
msgstr ""
#: admin/views/tab-advanced.php
msgid "No log entries yet. Enable debug mode to see activity logs."
msgstr ""
#: admin/views/tab-advanced.php
msgid "Type"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Message"
msgstr ""
#: admin/views/tab-advanced.php
msgid "Date"
msgstr ""
#: includes/class-shortcode.php
msgid "No Instagram posts found. Please sync your Instagram account in the admin settings."
msgstr ""
#: includes/class-shortcode.php
msgid "Follow on Instagram"
msgstr ""
#: includes/class-cron.php
msgid "Every 30 Minutes"
msgstr ""
#: includes/class-cron.php
msgid "Hourly"
msgstr ""
#: includes/class-cron.php
msgid "Every 6 Hours"
msgstr ""
#: includes/class-cron.php
msgid "Daily"
msgstr ""
#: includes/class-cron.php
msgid "Weekly"
msgstr ""
#: includes/class-logger.php
msgid "Success"
msgstr ""
#: includes/class-logger.php
msgid "Error"
msgstr ""
#: includes/class-logger.php
msgid "Warning"
msgstr ""
#: includes/class-logger.php
msgid "Info"
msgstr ""
#: includes/class-logger.php
msgid "ago"
msgstr ""

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,44 @@
/**
* Instagram Gallery Sync Pro - Block Editor Styles
*
* Styles for the Gutenberg block in the editor
*
* @package Instagram_Gallery_Sync_Pro
*/
/* Block placeholder */
.wp-block-igsp-instagram-gallery {
position: relative;
}
.igsp-block-preview {
min-height: 200px;
}
/* Editor-specific overrides */
.editor-styles-wrapper .igsp-gallery {
margin: 0;
}
/* Ensure hover effects don't interfere with editing */
.editor-styles-wrapper .igsp-item:hover .igsp-hover-overlay {
opacity: 0.3;
}
/* Block preview loading state */
.components-server-side-render__loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
background: #f5f5f5;
border-radius: 4px;
}
/* Block empty state in editor */
.editor-styles-wrapper .igsp-empty {
background: #f0f0f0;
border: 2px dashed #ccc;
text-align: center;
padding: 40px 20px;
}

408
public/css/gallery-base.css Normal file
View File

@@ -0,0 +1,408 @@
/**
* Instagram Gallery Sync Pro - Base Styles
*
* Core styles for all gallery layouts
*
* @package Instagram_Gallery_Sync_Pro
*/
/* ============================================
CSS Variables (customizable via admin)
============================================ */
:root {
--igsp-primary: #e1306c;
--igsp-hover: #c13584;
--igsp-text: #ffffff;
--igsp-font-size: 14px;
--igsp-radius: 0px;
--columns: 3;
--spacing: 10px;
}
/* ============================================
Gallery Container
============================================ */
.igsp-gallery {
width: 100%;
max-width: 100%;
margin: 0 auto;
box-sizing: border-box;
}
.igsp-gallery *,
.igsp-gallery *::before,
.igsp-gallery *::after {
box-sizing: border-box;
}
/* ============================================
Gallery Item
============================================ */
.igsp-item {
position: relative;
overflow: hidden;
background: #f0f0f0;
border-radius: var(--igsp-radius);
}
.igsp-link {
display: block;
text-decoration: none;
color: inherit;
}
.igsp-image-wrapper {
position: relative;
overflow: hidden;
width: 100%;
}
.igsp-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease, filter 0.4s ease;
}
/* Aspect Ratios */
.igsp-aspect-square .igsp-image-wrapper {
padding-bottom: 100%;
}
.igsp-aspect-4-3 .igsp-image-wrapper {
padding-bottom: 75%;
}
.igsp-aspect-16-9 .igsp-image-wrapper {
padding-bottom: 56.25%;
}
.igsp-aspect-3-4 .igsp-image-wrapper {
padding-bottom: 133.33%;
}
.igsp-aspect-square .igsp-image,
.igsp-aspect-4-3 .igsp-image,
.igsp-aspect-16-9 .igsp-image,
.igsp-aspect-3-4 .igsp-image {
position: absolute;
top: 0;
left: 0;
}
/* ============================================
Overlays
============================================ */
.igsp-overlay,
.igsp-hover-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
}
.igsp-overlay {
background: linear-gradient(transparent 50%, rgba(0, 0, 0, 0.7));
align-items: flex-end;
padding: 15px;
opacity: 1;
}
.igsp-hover-overlay {
background: var(--igsp-hover);
opacity: 0;
}
.igsp-item:hover .igsp-hover-overlay {
opacity: 0.8;
}
.igsp-caption {
color: var(--igsp-text);
font-size: var(--igsp-font-size);
line-height: 1.4;
text-align: left;
width: 100%;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.igsp-caption-below {
padding: 12px;
font-size: var(--igsp-font-size);
color: #333;
background: #fff;
border: 1px solid #eee;
border-top: none;
border-radius: 0 0 var(--igsp-radius) var(--igsp-radius);
}
.igsp-icon {
color: var(--igsp-text);
opacity: 0;
transform: scale(0.8);
transition: all 0.3s ease;
}
.igsp-icon svg {
width: 32px;
height: 32px;
}
.igsp-item:hover .igsp-icon {
opacity: 1;
transform: scale(1);
}
/* ============================================
Hover Effects
============================================ */
/* Zoom */
.igsp-hover-zoom .igsp-item:hover .igsp-image {
transform: scale(1.1);
}
/* Fade */
.igsp-hover-fade .igsp-overlay {
opacity: 0;
}
.igsp-hover-fade .igsp-item:hover .igsp-overlay {
opacity: 1;
}
/* Overlay with Icon */
.igsp-hover-overlay .igsp-hover-overlay {
opacity: 0;
}
/* Grayscale */
.igsp-hover-grayscale .igsp-image {
filter: grayscale(100%);
}
.igsp-hover-grayscale .igsp-item:hover .igsp-image {
filter: grayscale(0%);
}
/* Lift */
.igsp-hover-lift .igsp-item {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.igsp-hover-lift .igsp-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
/* ============================================
Follow Button
============================================ */
.igsp-follow-btn-wrapper {
text-align: center;
margin-top: 25px;
}
.igsp-follow-btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 28px;
background: var(--igsp-primary);
color: var(--igsp-text);
text-decoration: none;
border-radius: 25px;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
}
.igsp-follow-btn:hover {
background: var(--igsp-hover);
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(225, 48, 108, 0.3);
}
.igsp-follow-btn svg {
flex-shrink: 0;
}
/* ============================================
Empty State
============================================ */
.igsp-empty {
text-align: center;
padding: 40px 20px;
background: #f9f9f9;
border-radius: var(--igsp-radius);
color: #666;
}
/* ============================================
Loading States
============================================ */
.igsp-loading {
position: relative;
min-height: 200px;
}
.igsp-loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Spinner */
.igsp-loader-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid var(--igsp-primary);
border-radius: 50%;
animation: igsp-spin 1s linear infinite;
}
@keyframes igsp-spin {
to {
transform: rotate(360deg);
}
}
/* Dots */
.igsp-loader-dots {
display: flex;
gap: 8px;
}
.igsp-loader-dots span {
width: 10px;
height: 10px;
background: var(--igsp-primary);
border-radius: 50%;
animation: igsp-bounce 1.4s ease-in-out infinite both;
}
.igsp-loader-dots span:nth-child(2) {
animation-delay: 0.16s;
}
.igsp-loader-dots span:nth-child(3) {
animation-delay: 0.32s;
}
@keyframes igsp-bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* Pulse */
.igsp-loader-pulse {
width: 40px;
height: 40px;
background: var(--igsp-primary);
border-radius: 50%;
animation: igsp-pulse 1.5s ease-in-out infinite;
}
@keyframes igsp-pulse {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1.2);
opacity: 0;
}
}
/* Skeleton */
.igsp-skeleton .igsp-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: igsp-skeleton 1.5s ease-in-out infinite;
}
@keyframes igsp-skeleton {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Lazy load fade-in */
.igsp-image.lazyload {
opacity: 0;
transition: opacity 0.5s ease;
}
.igsp-image.loaded {
opacity: 1;
}
/* ============================================
Responsive Breakpoints
============================================ */
@media (max-width: 1024px) {
.igsp-gallery {
--columns: var(--columns-tablet, 2);
}
}
@media (max-width: 767px) {
.igsp-gallery {
--columns: var(--columns-mobile, 1);
}
.igsp-follow-btn {
width: 100%;
justify-content: center;
}
}
/* ============================================
Accessibility
============================================ */
.igsp-link:focus {
outline: 2px solid var(--igsp-primary);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.igsp-image,
.igsp-overlay,
.igsp-hover-overlay,
.igsp-icon,
.igsp-item,
.igsp-follow-btn {
transition: none;
}
.igsp-loader-spinner,
.igsp-loader-dots span,
.igsp-loader-pulse {
animation: none;
}
}

103
public/css/gallery-grid.css Normal file
View File

@@ -0,0 +1,103 @@
/**
* Instagram Gallery Sync Pro - Grid Layout
*
* Standard grid layout styles
*
* @package Instagram_Gallery_Sync_Pro
*/
.igsp-grid {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: var(--spacing);
}
/* Responsive grid columns */
@media (max-width: 1024px) {
.igsp-grid {
grid-template-columns: repeat(var(--columns-tablet, 2), 1fr);
}
}
@media (max-width: 767px) {
.igsp-grid {
grid-template-columns: repeat(var(--columns-mobile, 1), 1fr);
}
}
/* List layout */
.igsp-list {
display: flex;
flex-direction: column;
gap: var(--spacing);
}
.igsp-list .igsp-item {
display: flex;
flex-direction: row;
align-items: stretch;
}
.igsp-list .igsp-image-wrapper {
width: 200px;
flex-shrink: 0;
}
.igsp-list .igsp-caption-below {
flex: 1;
display: flex;
align-items: center;
padding: 20px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 var(--igsp-radius) var(--igsp-radius) 0;
}
@media (max-width: 600px) {
.igsp-list .igsp-item {
flex-direction: column;
}
.igsp-list .igsp-image-wrapper {
width: 100%;
}
.igsp-list .igsp-caption-below {
border-left: 1px solid #eee;
border-top: none;
border-radius: 0 0 var(--igsp-radius) var(--igsp-radius);
}
}
/* Justified layout */
.igsp-justified {
display: flex;
flex-wrap: wrap;
gap: var(--spacing);
}
.igsp-justified .igsp-item {
flex-grow: 1;
min-width: 200px;
max-width: 100%;
}
.igsp-justified .igsp-image-wrapper {
height: 250px;
}
.igsp-justified .igsp-image {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (max-width: 767px) {
.igsp-justified .igsp-item {
min-width: 100%;
}
.igsp-justified .igsp-image-wrapper {
height: 200px;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Instagram Gallery Sync Pro - Masonry Layout
*
* Pinterest-style masonry layout
*
* @package Instagram_Gallery_Sync_Pro
*/
.igsp-masonry {
display: block;
}
/* CSS-only masonry fallback */
.igsp-masonry.no-js {
column-count: var(--columns);
column-gap: var(--spacing);
}
.igsp-masonry.no-js .igsp-item {
break-inside: avoid;
margin-bottom: var(--spacing);
}
/* JS-powered masonry */
.igsp-masonry.masonry-initialized {
display: flex;
flex-wrap: wrap;
margin: calc(var(--spacing) / -2);
}
.igsp-masonry.masonry-initialized .igsp-item {
padding: calc(var(--spacing) / 2);
}
/* Natural height for masonry */
.igsp-masonry .igsp-image-wrapper {
padding-bottom: 0 !important;
}
.igsp-masonry .igsp-image {
position: relative !important;
height: auto !important;
}
/* Responsive masonry */
@media (max-width: 1024px) {
.igsp-masonry.no-js {
column-count: var(--columns-tablet, 2);
}
}
@media (max-width: 767px) {
.igsp-masonry.no-js {
column-count: var(--columns-mobile, 1);
}
}
/* Loading state */
.igsp-masonry.loading .igsp-item {
opacity: 0;
}
.igsp-masonry.loaded .igsp-item {
opacity: 1;
transition: opacity 0.3s ease;
}

View File

@@ -0,0 +1,131 @@
/**
* Instagram Gallery Sync Pro - Slider Layout
*
* Swiper-based carousel/slider styles
*
* @package Instagram_Gallery_Sync_Pro
*/
.igsp-slider {
position: relative;
overflow: hidden;
}
.igsp-slider .swiper {
padding-bottom: 40px;
}
.igsp-slider .swiper-slide {
width: auto;
height: auto;
}
.igsp-slider .swiper-slide .igsp-image-wrapper {
width: 300px;
height: 300px;
}
.igsp-slider .swiper-slide .igsp-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Navigation arrows */
.igsp-slider .swiper-button-prev,
.igsp-slider .swiper-button-next {
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.igsp-slider .swiper-button-prev::after,
.igsp-slider .swiper-button-next::after {
font-size: 18px;
font-weight: bold;
color: #333;
}
.igsp-slider .swiper-button-prev:hover,
.igsp-slider .swiper-button-next:hover {
background: var(--igsp-primary);
}
.igsp-slider .swiper-button-prev:hover::after,
.igsp-slider .swiper-button-next:hover::after {
color: #fff;
}
.igsp-slider .swiper-button-disabled {
opacity: 0.3;
}
/* Pagination */
.igsp-slider .swiper-pagination {
bottom: 0;
}
.igsp-slider .swiper-pagination-bullet {
width: 10px;
height: 10px;
background: #ccc;
opacity: 1;
transition: all 0.3s ease;
}
.igsp-slider .swiper-pagination-bullet-active {
background: var(--igsp-primary);
transform: scale(1.2);
}
/* Centered slides option */
.igsp-slider.centered .swiper-slide {
opacity: 0.5;
transform: scale(0.9);
transition: all 0.3s ease;
}
.igsp-slider.centered .swiper-slide-active {
opacity: 1;
transform: scale(1);
}
/* Responsive */
@media (max-width: 1024px) {
.igsp-slider .swiper-slide .igsp-image-wrapper {
width: 250px;
height: 250px;
}
}
@media (max-width: 767px) {
.igsp-slider .swiper-slide .igsp-image-wrapper {
width: 100%;
max-width: 300px;
height: auto;
padding-bottom: 100%;
}
.igsp-slider .swiper-button-prev,
.igsp-slider .swiper-button-next {
width: 36px;
height: 36px;
}
.igsp-slider .swiper-button-prev::after,
.igsp-slider .swiper-button-next::after {
font-size: 14px;
}
}
/* Hide navigation on touch devices */
@media (hover: none) {
.igsp-slider .swiper-button-prev,
.igsp-slider .swiper-button-next {
display: none;
}
}

137
public/css/lightbox.css Normal file
View File

@@ -0,0 +1,137 @@
/**
* Instagram Gallery Sync Pro - Lightbox Styles
*
* Custom styles for GLightbox
*
* @package Instagram_Gallery_Sync_Pro
*/
/* Override GLightbox styles */
.glightbox-container .gslide-media {
max-width: 90vw;
max-height: 90vh;
}
.glightbox-container .gslide-image img {
max-width: 90vw;
max-height: 85vh;
object-fit: contain;
}
.glightbox-container .gslide-description {
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px 30px;
}
.glightbox-container .gdesc-inner {
padding: 0;
}
.glightbox-container .gslide-title {
font-size: 14px;
color: #fff;
font-weight: normal;
line-height: 1.6;
margin: 0;
}
/* Navigation */
.glightbox-container .gnext,
.glightbox-container .gprev {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.glightbox-container .gnext svg,
.glightbox-container .gprev svg {
width: 24px;
height: 24px;
fill: #333;
}
.glightbox-container .gnext:hover,
.glightbox-container .gprev:hover {
background: var(--igsp-primary);
}
.glightbox-container .gnext:hover svg,
.glightbox-container .gprev:hover svg {
fill: #fff;
}
/* Close button */
.glightbox-container .gclose {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
top: 20px;
right: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.glightbox-container .gclose svg {
width: 20px;
height: 20px;
fill: #333;
}
.glightbox-container .gclose:hover {
background: var(--igsp-primary);
}
.glightbox-container .gclose:hover svg {
fill: #fff;
}
/* Counter */
.glightbox-container .gcounter {
font-size: 14px;
color: #fff;
background: rgba(0, 0, 0, 0.5);
padding: 5px 12px;
border-radius: 20px;
top: 25px;
left: 25px;
}
/* Loading */
.glightbox-container .gloader {
border-color: rgba(255, 255, 255, 0.2);
border-left-color: var(--igsp-primary);
}
/* Mobile adjustments */
@media (max-width: 767px) {
.glightbox-container .gnext,
.glightbox-container .gprev,
.glightbox-container .gclose {
width: 40px;
height: 40px;
}
.glightbox-container .gnext svg,
.glightbox-container .gprev svg {
width: 18px;
height: 18px;
}
.glightbox-container .gclose svg {
width: 16px;
height: 16px;
}
.glightbox-container .gclose {
top: 10px;
right: 10px;
}
.glightbox-container .gcounter {
top: 15px;
left: 15px;
}
}

1
public/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden

165
public/js/block-editor.js Normal file
View File

@@ -0,0 +1,165 @@
/**
* Instagram Gallery Sync Pro - Gutenberg Block Editor
*
* Block registration and editor UI
*
* @package Instagram_Gallery_Sync_Pro
*/
(function (blocks, element, blockEditor, components, serverSideRender, i18n) {
'use strict';
const { registerBlockType } = blocks;
const { createElement: el, Fragment } = element;
const { InspectorControls, useBlockProps } = blockEditor;
const { PanelBody, SelectControl, RangeControl, ToggleControl, Placeholder } = components;
const ServerSideRender = serverSideRender;
const { __ } = i18n;
// Get localized settings
const settings = window.igspBlock || {};
const strings = settings.strings || {};
/**
* Block Icon
*/
const blockIcon = el('svg', {
viewBox: '0 0 24 24',
xmlns: 'http://www.w3.org/2000/svg'
},
el('path', {
d: 'M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z'
})
);
/**
* Register Block
*/
registerBlockType('igsp/instagram-gallery', {
title: strings.title || 'Instagram Gallery',
description: strings.description || 'Display your Instagram photos in a beautiful gallery.',
icon: blockIcon,
category: 'widgets',
keywords: ['instagram', 'gallery', 'photos', 'social'],
supports: {
align: ['wide', 'full'],
html: false
},
/**
* Edit Component
*/
edit: function (props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps();
// Inspector controls
const inspectorControls = el(InspectorControls, {},
el(PanelBody, {
title: __('Gallery Settings', 'instagram-gallery-sync-pro'),
initialOpen: true
},
el(SelectControl, {
label: strings.layout || 'Layout',
value: attributes.layout,
options: settings.layouts || [
{ value: 'grid', label: 'Grid' },
{ value: 'masonry', label: 'Masonry' },
{ value: 'slider', label: 'Slider' }
],
onChange: function (value) {
setAttributes({ layout: value });
}
}),
el(RangeControl, {
label: strings.columns || 'Columns',
value: attributes.columns,
min: 1,
max: 6,
onChange: function (value) {
setAttributes({ columns: value });
}
}),
el(RangeControl, {
label: strings.spacing || 'Spacing',
value: attributes.spacing,
min: 0,
max: 50,
onChange: function (value) {
setAttributes({ spacing: value });
}
}),
el(RangeControl, {
label: strings.limit || 'Number of Images',
value: attributes.limit,
min: 1,
max: 50,
onChange: function (value) {
setAttributes({ limit: value });
}
}),
el(SelectControl, {
label: strings.order || 'Order',
value: attributes.order,
options: settings.orders || [
{ value: 'newest', label: 'Newest First' },
{ value: 'oldest', label: 'Oldest First' },
{ value: 'random', label: 'Random' }
],
onChange: function (value) {
setAttributes({ order: value });
}
})
),
el(PanelBody, {
title: __('Display Options', 'instagram-gallery-sync-pro'),
initialOpen: false
},
el(ToggleControl, {
label: strings.lightbox || 'Enable Lightbox',
checked: attributes.lightbox,
onChange: function (value) {
setAttributes({ lightbox: value });
}
}),
el(ToggleControl, {
label: strings.captions || 'Show Captions',
checked: attributes.captions,
onChange: function (value) {
setAttributes({ captions: value });
}
})
)
);
// Block preview
const preview = el(ServerSideRender, {
block: 'igsp/instagram-gallery',
attributes: attributes,
className: 'igsp-block-preview'
});
return el(Fragment, {},
inspectorControls,
el('div', blockProps,
preview
)
);
},
/**
* Save - Uses server-side rendering
*/
save: function () {
return null;
}
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor,
window.wp.components,
window.wp.serverSideRender,
window.wp.i18n
);

View File

@@ -0,0 +1,257 @@
/**
* Instagram Gallery Sync Pro - Frontend Script
*
* Handles gallery initialization, masonry, lightbox, and lazy loading
*
* @package Instagram_Gallery_Sync_Pro
*/
(function ($) {
'use strict';
/**
* Gallery Controller
*/
const IGSPGallery = {
/**
* Initialize all galleries on page
*/
init: function () {
const self = this;
// Wait for DOM ready
$(document).ready(function () {
self.initGalleries();
});
},
/**
* Initialize individual galleries
*/
initGalleries: function () {
const self = this;
$('.igsp-gallery').each(function () {
const $gallery = $(this);
const layout = $gallery.data('layout');
// Set responsive CSS variables
self.setResponsiveVariables($gallery);
// Initialize based on layout
switch (layout) {
case 'masonry':
self.initMasonry($gallery);
break;
case 'slider':
self.initSlider($gallery);
break;
default:
self.initGrid($gallery);
}
// Initialize lightbox if enabled
if (typeof igspFrontend !== 'undefined' && igspFrontend.lightbox) {
self.initLightbox($gallery);
}
// Initialize lazy loading
self.initLazyLoad($gallery);
});
},
/**
* Set responsive CSS variables
*/
setResponsiveVariables: function ($gallery) {
const columnsTablet = $gallery.data('columns-tablet') || 2;
const columnsMobile = $gallery.data('columns-mobile') || 1;
$gallery.css({
'--columns-tablet': columnsTablet,
'--columns-mobile': columnsMobile
});
},
/**
* Initialize grid layout
*/
initGrid: function ($gallery) {
$gallery.addClass('loaded');
},
/**
* Initialize masonry layout
*/
initMasonry: function ($gallery) {
if (typeof Masonry === 'undefined' || typeof imagesLoaded === 'undefined') {
// Fallback to CSS-only masonry
$gallery.addClass('no-js loaded');
return;
}
$gallery.addClass('loading');
// Wait for images to load
imagesLoaded($gallery[0], function () {
const columns = parseInt($gallery.data('columns')) || 3;
const spacing = parseInt($gallery.data('spacing')) || 10;
// Calculate item width
const columnWidth = Math.floor(($gallery.width() - (spacing * (columns - 1))) / columns);
const msnry = new Masonry($gallery[0], {
itemSelector: '.igsp-item',
columnWidth: columnWidth,
gutter: spacing,
percentPosition: false,
transitionDuration: '0.3s'
});
$gallery.removeClass('loading').addClass('masonry-initialized loaded');
// Handle responsive
let resizeTimeout;
$(window).on('resize', function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function () {
msnry.layout();
}, 250);
});
});
},
/**
* Initialize slider layout
*/
initSlider: function ($gallery) {
if (typeof Swiper === 'undefined') {
console.warn('IGSP: Swiper library not loaded');
return;
}
const $swiperContainer = $gallery.find('.swiper');
if (!$swiperContainer.length) {
return;
}
const autoplay = typeof igspFrontend !== 'undefined' && igspFrontend.autoplay;
new Swiper($swiperContainer[0], {
slidesPerView: 'auto',
spaceBetween: parseInt($gallery.data('spacing')) || 10,
loop: true,
centeredSlides: false,
autoplay: autoplay ? {
delay: 4000,
disableOnInteraction: false
} : false,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
},
breakpoints: {
320: {
slidesPerView: 1,
centeredSlides: true
},
640: {
slidesPerView: 2,
centeredSlides: false
},
1024: {
slidesPerView: 3,
centeredSlides: false
}
}
});
$gallery.addClass('loaded');
},
/**
* Initialize lightbox
*/
initLightbox: function ($gallery) {
if (typeof GLightbox === 'undefined') {
console.warn('IGSP: GLightbox library not loaded');
return;
}
const lightbox = GLightbox({
selector: '.igsp-gallery [data-gallery="igsp-lightbox"]',
touchNavigation: true,
loop: true,
autoplayVideos: false,
openEffect: 'zoom',
closeEffect: 'zoom',
cssEfects: {
fade: { in: 'fadeIn', out: 'fadeOut' },
zoom: { in: 'zoomIn', out: 'zoomOut' }
}
});
},
/**
* Initialize lazy loading
*/
initLazyLoad: function ($gallery) {
const $lazyImages = $gallery.find('.igsp-image.lazyload');
if (!$lazyImages.length) {
return;
}
// Use native lazy loading if supported
if ('loading' in HTMLImageElement.prototype) {
$lazyImages.each(function () {
const $img = $(this);
$img.attr('src', $img.data('src'));
$img.removeClass('lazyload').addClass('loaded');
});
return;
}
// Fallback: Intersection Observer
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazyload');
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, {
rootMargin: '100px'
});
$lazyImages.each(function () {
observer.observe(this);
});
} else {
// Fallback: Load all images immediately
$lazyImages.each(function () {
const $img = $(this);
$img.attr('src', $img.data('src'));
$img.removeClass('lazyload').addClass('loaded');
});
}
}
};
// Initialize
IGSPGallery.init();
// Make available globally
window.IGSPGallery = IGSPGallery;
})(jQuery);

1
public/js/glightbox.min.js vendored Normal file

File diff suppressed because one or more lines are too long

12
public/js/imagesloaded.pkgd.min.js vendored Normal file
View File

@@ -0,0 +1,12 @@
/*!
* imagesLoaded PACKAGED v5.0.0
* JavaScript is all like "You images are done yet or what?"
* MIT License
*/
!function(t,e){"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,(function(){function t(){}let e=t.prototype;return e.on=function(t,e){if(!t||!e)return this;let i=this._events=this._events||{},s=i[t]=i[t]||[];return s.includes(e)||s.push(e),this},e.once=function(t,e){if(!t||!e)return this;this.on(t,e);let i=this._onceEvents=this._onceEvents||{};return(i[t]=i[t]||{})[e]=!0,this},e.off=function(t,e){let i=this._events&&this._events[t];if(!i||!i.length)return this;let s=i.indexOf(e);return-1!=s&&i.splice(s,1),this},e.emitEvent=function(t,e){let i=this._events&&this._events[t];if(!i||!i.length)return this;i=i.slice(0),e=e||[];let s=this._onceEvents&&this._onceEvents[t];for(let n of i){s&&s[n]&&(this.off(t,n),delete s[n]),n.apply(this,e)}return this},e.allOff=function(){return delete this._events,delete this._onceEvents,this},t})),
/*!
* imagesLoaded v5.0.0
* JavaScript is all like "You images are done yet or what?"
* MIT License
*/
function(t,e){"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}("undefined"!=typeof window?window:this,(function(t,e){let i=t.jQuery,s=t.console;function n(t,e,o){if(!(this instanceof n))return new n(t,e,o);let r=t;var h;("string"==typeof t&&(r=document.querySelectorAll(t)),r)?(this.elements=(h=r,Array.isArray(h)?h:"object"==typeof h&&"number"==typeof h.length?[...h]:[h]),this.options={},"function"==typeof e?o=e:Object.assign(this.options,e),o&&this.on("always",o),this.getImages(),i&&(this.jqDeferred=new i.Deferred),setTimeout(this.check.bind(this))):s.error(`Bad element for imagesLoaded ${r||t}`)}n.prototype=Object.create(e.prototype),n.prototype.getImages=function(){this.images=[],this.elements.forEach(this.addElementImages,this)};const o=[1,9,11];n.prototype.addElementImages=function(t){"IMG"===t.nodeName&&this.addImage(t),!0===this.options.background&&this.addElementBackgroundImages(t);let{nodeType:e}=t;if(!e||!o.includes(e))return;let i=t.querySelectorAll("img");for(let t of i)this.addImage(t);if("string"==typeof this.options.background){let e=t.querySelectorAll(this.options.background);for(let t of e)this.addElementBackgroundImages(t)}};const r=/url\((['"])?(.*?)\1\)/gi;function h(t){this.img=t}function d(t,e){this.url=t,this.element=e,this.img=new Image}return n.prototype.addElementBackgroundImages=function(t){let e=getComputedStyle(t);if(!e)return;let i=r.exec(e.backgroundImage);for(;null!==i;){let s=i&&i[2];s&&this.addBackground(s,t),i=r.exec(e.backgroundImage)}},n.prototype.addImage=function(t){let e=new h(t);this.images.push(e)},n.prototype.addBackground=function(t,e){let i=new d(t,e);this.images.push(i)},n.prototype.check=function(){if(this.progressedCount=0,this.hasAnyBroken=!1,!this.images.length)return void this.complete();let t=(t,e,i)=>{setTimeout((()=>{this.progress(t,e,i)}))};this.images.forEach((function(e){e.once("progress",t),e.check()}))},n.prototype.progress=function(t,e,i){this.progressedCount++,this.hasAnyBroken=this.hasAnyBroken||!t.isLoaded,this.emitEvent("progress",[this,t,e]),this.jqDeferred&&this.jqDeferred.notify&&this.jqDeferred.notify(this,t),this.progressedCount===this.images.length&&this.complete(),this.options.debug&&s&&s.log(`progress: ${i}`,t,e)},n.prototype.complete=function(){let t=this.hasAnyBroken?"fail":"done";if(this.isComplete=!0,this.emitEvent(t,[this]),this.emitEvent("always",[this]),this.jqDeferred){let t=this.hasAnyBroken?"reject":"resolve";this.jqDeferred[t](this)}},h.prototype=Object.create(e.prototype),h.prototype.check=function(){this.getIsImageComplete()?this.confirm(0!==this.img.naturalWidth,"naturalWidth"):(this.proxyImage=new Image,this.img.crossOrigin&&(this.proxyImage.crossOrigin=this.img.crossOrigin),this.proxyImage.addEventListener("load",this),this.proxyImage.addEventListener("error",this),this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.proxyImage.src=this.img.currentSrc||this.img.src)},h.prototype.getIsImageComplete=function(){return this.img.complete&&this.img.naturalWidth},h.prototype.confirm=function(t,e){this.isLoaded=t;let{parentNode:i}=this.img,s="PICTURE"===i.nodeName?i:this.img;this.emitEvent("progress",[this,s,e])},h.prototype.handleEvent=function(t){let e="on"+t.type;this[e]&&this[e](t)},h.prototype.onload=function(){this.confirm(!0,"onload"),this.unbindEvents()},h.prototype.onerror=function(){this.confirm(!1,"onerror"),this.unbindEvents()},h.prototype.unbindEvents=function(){this.proxyImage.removeEventListener("load",this),this.proxyImage.removeEventListener("error",this),this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype=Object.create(h.prototype),d.prototype.check=function(){this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.img.src=this.url,this.getIsImageComplete()&&(this.confirm(0!==this.img.naturalWidth,"naturalWidth"),this.unbindEvents())},d.prototype.unbindEvents=function(){this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype.confirm=function(t,e){this.isLoaded=t,this.emitEvent("progress",[this,this.element,e])},n.makeJQueryPlugin=function(e){(e=e||t.jQuery)&&(i=e,i.fn.imagesLoaded=function(t,e){return new n(this,t,e).jqDeferred.promise(i(this))})},n.makeJQueryPlugin(),n}));

9
public/js/masonry.pkgd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14
public/js/swiper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

156
uninstall.php Normal file
View File

@@ -0,0 +1,156 @@
<?php
/**
* Uninstall Script for Instagram Gallery Sync Pro
*
* This file runs when the plugin is deleted from WordPress.
* It removes all plugin data including:
* - Custom database tables
* - Plugin options
* - Uploaded images
* - Transients
*
* @package Instagram_Gallery_Sync_Pro
*/
// Exit if not called by WordPress
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Require WordPress database class
global $wpdb;
/**
* Remove all plugin options
*/
$options_to_delete = array(
// Instagram settings
'igsp_username',
'igsp_max_images',
'igsp_save_locally',
'igsp_image_quality',
'igsp_sync_interval',
'igsp_last_sync',
// Layout settings
'igsp_layout_type',
'igsp_columns_desktop',
'igsp_columns_tablet',
'igsp_columns_mobile',
'igsp_spacing',
'igsp_padding',
'igsp_aspect_ratio',
'igsp_object_fit',
'igsp_border_radius',
'igsp_hover_effect',
// Display settings
'igsp_display_limit',
'igsp_order',
'igsp_show_caption',
'igsp_caption_position',
'igsp_show_instagram_btn',
'igsp_link_behavior',
'igsp_lightbox',
'igsp_lazy_loading',
'igsp_loader_type',
// Styling
'igsp_primary_color',
'igsp_hover_color',
'igsp_text_color',
'igsp_caption_font_size',
'igsp_custom_css',
'igsp_css_prefix',
// Advanced
'igsp_debug_mode',
'igsp_cache_duration',
'igsp_request_timeout',
'igsp_user_agent',
'igsp_proxy_host',
'igsp_proxy_port',
'igsp_auto_delete_days',
// Database version
'igsp_db_version',
);
foreach ($options_to_delete as $option) {
delete_option($option);
}
/**
* Drop custom database tables
*/
$tables = array(
$wpdb->prefix . 'instagram_gallery_posts',
$wpdb->prefix . 'instagram_gallery_log',
);
foreach ($tables as $table) {
$wpdb->query("DROP TABLE IF EXISTS {$table}");
}
/**
* Delete all transients
*/
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_igsp_%'
OR option_name LIKE '_transient_timeout_igsp_%'"
);
/**
* Remove uploaded images directory
*/
$upload_dir = wp_upload_dir();
$instagram_dir = $upload_dir['basedir'] . '/instagram-gallery';
if (is_dir($instagram_dir)) {
igsp_recursive_delete($instagram_dir);
}
/**
* Recursively delete a directory and its contents
*
* @param string $dir Directory path
* @return bool
*/
function igsp_recursive_delete($dir)
{
if (!is_dir($dir)) {
return false;
}
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
igsp_recursive_delete($path);
} else {
unlink($path);
}
}
return rmdir($dir);
}
/**
* Clear scheduled cron events
*/
$timestamp = wp_next_scheduled('igsp_sync_instagram');
if ($timestamp) {
wp_unschedule_event($timestamp, 'igsp_sync_instagram');
}
// Clear any remaining cron events
wp_clear_scheduled_hook('igsp_sync_instagram');
wp_clear_scheduled_hook('igsp_cleanup_logs');
/**
* Flush rewrite rules
*/
flush_rewrite_rules();