diff --git a/api/list_images.php b/api/list_images.php new file mode 100644 index 0000000..d790a4e --- /dev/null +++ b/api/list_images.php @@ -0,0 +1,33 @@ + false, 'error' => 'img directory not found']); + exit; +} + +$allowedExt = ['jpg','jpeg','png','gif','webp']; +$images = []; + +$files = scandir($imgDir); +if ($files === false) $files = []; + +foreach ($files as $f) { + if ($f === '.' || $f === '..') continue; + $path = $imgDir . DIRECTORY_SEPARATOR . $f; + if (!is_file($path)) continue; + $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION)); + if (!in_array($ext, $allowedExt, true)) continue; + $images[] = $f; +} + +// stable order +natcasesort($images); +$images = array_values($images); + +echo json_encode(['ok' => true, 'images' => $images], JSON_UNESCAPED_UNICODE); + + diff --git a/api/upload_image.php b/api/upload_image.php new file mode 100644 index 0000000..e8a95b3 --- /dev/null +++ b/api/upload_image.php @@ -0,0 +1,87 @@ + false, 'error' => 'No file uploaded']); + exit; +} + +$file = $_FILES['image']; +if (!is_array($file) || !isset($file['error'])) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Invalid upload']); + exit; +} + +if ($file['error'] !== UPLOAD_ERR_OK) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Upload error: ' . $file['error']]); + exit; +} + +if (!isset($file['size']) || $file['size'] <= 0 || $file['size'] > $maxBytes) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'File too large (max 10MB)']); + exit; +} + +// Validate image content +$tmp = $file['tmp_name']; +$info = @getimagesize($tmp); +if ($info === false) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Not a valid image']); + exit; +} + +$mime = isset($info['mime']) ? strtolower($info['mime']) : ''; +$allowedMimeToExt = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', +]; +if (!isset($allowedMimeToExt[$mime])) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Unsupported image type']); + exit; +} + +$imgDir = realpath(__DIR__ . '/../img'); +if ($imgDir === false || !is_dir($imgDir)) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => 'img directory not found']); + exit; +} + +// Generate safe unique filename +$ext = $allowedMimeToExt[$mime]; +$base = 'upload_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)); +$filename = $base . '.' . $ext; +$dest = $imgDir . DIRECTORY_SEPARATOR . $filename; + +// Ensure we don't overwrite (extremely unlikely) +$tries = 0; +while (file_exists($dest) && $tries < 5) { + $filename = $base . '_' . bin2hex(random_bytes(2)) . '.' . $ext; + $dest = $imgDir . DIRECTORY_SEPARATOR . $filename; + $tries++; +} + +if (!move_uploaded_file($tmp, $dest)) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => 'Failed to save file (check permissions)']); + exit; +} + +// Conservative permissions +@chmod($dest, 0644); + +echo json_encode(['ok' => true, 'filename' => $filename], JSON_UNESCAPED_UNICODE); + + diff --git a/css/galleriffic-2.css b/css/galleriffic-2.css index 81daa05..44f1aac 100644 --- a/css/galleriffic-2.css +++ b/css/galleriffic-2.css @@ -100,6 +100,84 @@ div.navigation { /* The navigation style is set using jQuery so that the javascript specific styles won't be applied unless javascript is enabled. */ } +/* Upload panel under thumbnails */ +.upload-panel { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid #e5e5e5; +} +.upload-title { + font-weight: bold; + color: #333; + margin-bottom: 8px; +} +.dropzone { + position: relative; + border: 2px dashed #cfcfcf; + border-radius: 8px; + padding: 14px 12px; + background: #fafafa; + cursor: pointer; +} +.dropzone:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(46, 131, 255, 0.25); + border-color: #2e83ff; +} +.dropzone.dragover { + border-color: #2e83ff; + background: #f2f7ff; +} +.dropzone-text { + color: #666; + font-size: 12px; +} +.file-input { + position: absolute; + inset: 0; + opacity: 0; + width: 100%; + height: 100%; + cursor: pointer; +} +.upload-preview { + margin-top: 10px; +} +.upload-preview img { + max-width: 100%; + max-height: 160px; + border: 1px solid #ddd; + background: #fff; + display: block; +} +.preview-meta { + font-size: 11px; + color: #666; + margin-top: 6px; + word-break: break-all; +} +.upload-actions { + margin-top: 10px; + display: flex; + align-items: center; + gap: 10px; +} +.upload-actions button { + padding: 7px 12px; + border-radius: 6px; + border: 1px solid #cfcfcf; + background: #fff; + cursor: pointer; +} +.upload-actions button:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.upload-status { + font-size: 12px; + color: #666; +} + /* ---- Layout: slideshow (left) + thumbs (right), full width ---- */ .gallery-wrap { display: flex; diff --git a/index.html b/index.html index 938a716..e126d04 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,7 @@ +