Added galery

This commit is contained in:
2026-01-26 10:37:06 +01:00
parent d9b4c73baa
commit 33243fbe4a
31 changed files with 2508 additions and 229 deletions

View File

@@ -41,12 +41,9 @@ if (!$response_data->success || $response_data->score < 0.5) {
- Bessere User Experience
- Intelligente Bot-Erkennung durch Verhaltensanalyse
```
<!-- Screenshot Platzhalter -->
[📸 Screenshot: Registrierungsformular]
[📸 Screenshot: reCAPTCHA Badge]
```
![img.png](assets/screenshots/img.png)
![img.png](assets/screenshots/img2.png)
![img.png](assets/screenshots/img3.png)
---
## Erweiterung der Admin-Funktionen
@@ -123,6 +120,79 @@ Implementierung einer vollständigen Notizen-Anwendung mit CRUD-Funktionalität
---
## Bildergalerie mit Verschlüsselung
#### Sichere Bildergalerie mit AES-256 Verschlüsselung
**Beschreibung:**
Implementierung einer vollständigen Bildergalerie mit clientseitiger Vorschau und serverseitiger AES-256-CBC Verschlüsselung. Alle hochgeladenen Bilder werden verschlüsselt gespeichert und erst beim Abruf entschlüsselt.
**Features:**
- **AES-256-CBC Verschlüsselung**: Jedes Bild wird mit einem einzigartigen Schlüssel verschlüsselt
- **Automatische Thumbnail-Generierung**: Optimierte Vorschaubilder für schnelles Laden
- **Drag & Drop Upload**: Moderne Upload-Oberfläche mit Datei-Drag & Drop
- **AJAX-Upload mit Fortschrittsanzeige**: Echtzeit-Feedback während des Uploads
- **Öffentliche/Private Bilder**: Benutzer können Sichtbarkeit ihrer Bilder steuern
- **Bildvorschau vor Upload**: Clientseitige Vorschau des ausgewählten Bildes
**Technische Umsetzung:**
*Verschlüsselung (Upload):*
```php
$encryption_key = bin2hex(random_bytes(32));
$iv = random_bytes(openssl_cipher_iv_length('AES-256-CBC'));
$encrypted_image = openssl_encrypt($image_data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
$encrypted_image = base64_encode($iv . $encrypted_image);
```
*Entschlüsselung (Anzeige):*
```php
$encrypted_data = base64_decode(file_get_contents($filepath));
$iv = substr($encrypted_data, 0, $iv_length);
$encrypted_data = substr($encrypted_data, $iv_length);
$decrypted = openssl_decrypt($encrypted_data, 'AES-256-CBC', $encryption_key, OPENSSL_RAW_DATA, $iv);
```
**Datenbankstruktur:**
```sql
CREATE TABLE `gallery` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`filename` varchar(255) NOT NULL,
`thumb_filename` varchar(255) NOT NULL,
`title` varchar(255) NOT NULL,
`description` text,
`is_public` tinyint(1) NOT NULL DEFAULT 1,
`file_size` int(11) NOT NULL DEFAULT 0,
`mime_type` varchar(100) NOT NULL,
`encryption_key` varchar(64) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
```
**Unterstützte Formate:**
- JPEG / JPG
- PNG (mit Transparenz-Erhaltung)
- GIF
- WebP
**Zugriffsrechte:**
- **Öffentliche Galerie**: Alle öffentlichen Bilder für jeden sichtbar
- **Meine Bilder**: Nur eigene Bilder (öffentlich und privat)
- **Upload/Bearbeiten/Löschen**: Nur für angemeldete Benutzer
```
<!-- Screenshot Platzhalter -->
[📸 Screenshot: Galerie-Übersicht mit Bildraster]
[📸 Screenshot: Upload-Formular mit Drag & Drop Zone]
[📸 Screenshot: Upload-Fortschrittsanzeige]
[📸 Screenshot: Einzelbildansicht]
[📸 Screenshot: Meine Bilder - Verwaltung]
```
---
## jQuery - Einführung und Grundlagen
#### JavaScript Basics

View File

@@ -0,0 +1,25 @@
-- Gallery table for encrypted image uploads
-- Run this SQL to create/update the gallery table
-- Drop old table if exists and recreate
DROP TABLE IF EXISTS `gallery`;
CREATE TABLE `gallery` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`filename` varchar(255) NOT NULL,
`thumb_filename` varchar(255) NOT NULL,
`title` varchar(255) NOT NULL,
`description` text,
`is_public` tinyint(1) NOT NULL DEFAULT 1,
`file_size` int(11) NOT NULL DEFAULT 0,
`mime_type` varchar(100) NOT NULL,
`encryption_key` varchar(64) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `is_public` (`is_public`),
KEY `created_at` (`created_at`),
CONSTRAINT `gallery_user_fk` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -47,6 +47,8 @@ return array(
*/
'PATH_AVATARS' => realpath(dirname(__FILE__).'/../../') . '/public/avatars/',
'PATH_AVATARS_PUBLIC' => 'avatars/',
'PATH_GALLERY' => dirname(__FILE__) . '/../../public/gallery_uploads/',
'PATH_GALLERY_PUBLIC' => 'gallery_uploads/',
/**
* Configuration for: Default controller and action
*/

View File

@@ -134,6 +134,22 @@ class DatabaseController extends Controller
]);
}
/**
* Get columns for a specific table (AJAX endpoint)
* @param string $database_name
* @param string $table_name
*/
public function getColumns($database_name, $table_name)
{
$columns = TableModel::getTableColumns($database_name, $table_name);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'columns' => $columns
]);
}
/**
* Export database as raw SQL text
* @param string $database_name

View File

@@ -0,0 +1,226 @@
<?php
class GalleryController extends Controller
{
public function __construct()
{
parent::__construct();
}
public function index($page = 1)
{
$page = (int)$page;
$per_page = 24;
$this->View->render('gallery/index', array(
'images' => GalleryModel::getAllImages(null, $page, $per_page),
'total_images' => GalleryModel::getImageCount(),
'current_page' => $page,
'per_page' => $per_page
));
}
public function my($page = 1)
{
Auth::checkAuthentication();
$page = (int)$page;
$per_page = 24;
$user_id = Session::get('user_id');
$this->View->render('gallery/my', array(
'images' => GalleryModel::getAllImages($user_id, $page, $per_page),
'total_images' => GalleryModel::getImageCount($user_id),
'current_page' => $page,
'per_page' => $per_page
));
}
public function view($image_id)
{
$image = GalleryModel::getImage($image_id);
if (!$image) {
Redirect::to('gallery');
return;
}
if (!$image->is_public && $image->user_id != Session::get('user_id')) {
Session::add('feedback_negative', 'This image is private');
Redirect::to('gallery');
return;
}
$this->View->render('gallery/view', array(
'image' => $image
));
}
public function upload()
{
// Check if AJAX request first
$isAjax = $this->isAjaxRequest();
// Check authentication - return JSON error for AJAX
if (!Session::userIsLoggedIn()) {
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'You must be logged in to upload']);
return;
}
Redirect::to('login/index');
return;
}
// Handle POST request (form submission)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check if image was uploaded
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
$errorMsg = 'Please select an image to upload';
if (isset($_FILES['image'])) {
switch ($_FILES['image']['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$errorMsg = 'File is too large';
break;
case UPLOAD_ERR_NO_FILE:
$errorMsg = 'No file was uploaded';
break;
}
}
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => $errorMsg]);
return;
}
Session::add('feedback_negative', $errorMsg);
$this->View->render('gallery/upload');
return;
}
$title = Request::post('title');
$description = Request::post('description');
$is_public = Request::post('is_public') ? 1 : 0;
$image_id = GalleryModel::uploadImage($_FILES['image'], $title, $description, $is_public);
if ($isAjax) {
header('Content-Type: application/json');
if ($image_id) {
echo json_encode([
'success' => true,
'message' => 'Image uploaded successfully',
'image_id' => $image_id
]);
} else {
echo json_encode([
'success' => false,
'message' => Session::get('feedback_negative')[0] ?? 'Failed to upload image'
]);
}
return;
}
if ($image_id) {
Redirect::to('gallery/success/' . $image_id);
return;
}
}
$this->View->render('gallery/upload');
}
public function success($image_id)
{
Auth::checkAuthentication();
$image = GalleryModel::getImage($image_id);
if (!$image || $image->user_id != Session::get('user_id')) {
Redirect::to('gallery');
return;
}
$this->View->render('gallery/success', array(
'image' => $image
));
}
public function edit($image_id)
{
Auth::checkAuthentication();
$image = GalleryModel::getImage($image_id);
if (!$image || $image->user_id != Session::get('user_id')) {
Session::add('feedback_negative', 'Image not found or access denied');
Redirect::to('gallery/my');
return;
}
if (Request::post('submit_edit')) {
$title = Request::post('title');
$description = Request::post('description');
$is_public = Request::post('is_public') ? 1 : 0;
if (GalleryModel::updateImage($image_id, $title, $description, $is_public)) {
Session::add('feedback_positive', 'Image updated successfully');
Redirect::to('gallery/view/' . $image_id);
return;
}
}
$this->View->render('gallery/edit', array(
'image' => $image
));
}
public function delete($image_id)
{
Auth::checkAuthentication();
$success = GalleryModel::deleteImage($image_id);
if ($this->isAjaxRequest()) {
header('Content-Type: application/json');
echo json_encode([
'success' => $success,
'message' => $success ? 'Image deleted successfully' : 'Failed to delete image'
]);
return;
}
if ($success) {
Session::add('feedback_positive', 'Image deleted successfully');
} else {
Session::add('feedback_negative', 'Failed to delete image');
}
Redirect::to('gallery/my');
}
public function image($image_id, $type = 'full')
{
$thumbnail = ($type === 'thumb');
$result = GalleryModel::getDecryptedImage($image_id, $thumbnail);
if (!$result || !$result['data']) {
header('HTTP/1.0 404 Not Found');
exit;
}
header('Content-Type: ' . $result['mime_type']);
header('Content-Length: ' . strlen($result['data']));
header('Cache-Control: public, max-age=31536000');
echo $result['data'];
exit;
}
private function isAjaxRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}

View File

@@ -47,6 +47,11 @@ class View
public $privileges;
public $current_user;
/* Gallery properties */
public $images;
public $total_images;
public $image;
/**
* Static property to track if header has been rendered
*/

View File

@@ -0,0 +1,323 @@
<?php
class GalleryModel
{
private static $cipher = 'AES-256-CBC';
public static function getAllImages($user_id = null, $page = 1, $per_page = 20)
{
$database = DatabaseFactory::getFactory()->getConnection();
$offset = ($page - 1) * $per_page;
if ($user_id) {
$sql = "SELECT g.*, u.user_name
FROM gallery g
JOIN users u ON g.user_id = u.user_id
WHERE g.user_id = :user_id
ORDER BY g.created_at DESC
LIMIT :offset, :per_page";
$query = $database->prepare($sql);
$query->bindParam(':user_id', $user_id, PDO::PARAM_INT);
} else {
$sql = "SELECT g.*, u.user_name
FROM gallery g
JOIN users u ON g.user_id = u.user_id
WHERE g.is_public = 1
ORDER BY g.created_at DESC
LIMIT :offset, :per_page";
$query = $database->prepare($sql);
}
$query->bindParam(':offset', $offset, PDO::PARAM_INT);
$query->bindParam(':per_page', $per_page, PDO::PARAM_INT);
$query->execute();
return $query->fetchAll(PDO::FETCH_OBJ);
}
public static function getImageCount($user_id = null)
{
$database = DatabaseFactory::getFactory()->getConnection();
if ($user_id) {
$sql = "SELECT COUNT(*) as count FROM gallery WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id));
} else {
$sql = "SELECT COUNT(*) as count FROM gallery WHERE is_public = 1";
$query = $database->prepare($sql);
$query->execute();
}
$result = $query->fetch(PDO::FETCH_ASSOC);
return (int)$result['count'];
}
public static function getImage($image_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT g.*, u.user_name
FROM gallery g
JOIN users u ON g.user_id = u.user_id
WHERE g.id = :image_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':image_id' => $image_id));
return $query->fetch(PDO::FETCH_OBJ);
}
public static function uploadImage($file, $title, $description, $is_public)
{
$user_id = Session::get('user_id');
if (!$user_id) {
Session::add('feedback_negative', 'You must be logged in to upload');
return false;
}
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file['type'], $allowed_types)) {
Session::add('feedback_negative', 'Invalid file type. Allowed: JPG, PNG, GIF, WebP');
return false;
}
$max_size = 10 * 1024 * 1024;
if ($file['size'] > $max_size) {
Session::add('feedback_negative', 'File too large. Maximum size: 10MB');
return false;
}
$image_data = file_get_contents($file['tmp_name']);
if ($image_data === false) {
Session::add('feedback_negative', 'Failed to read uploaded file');
return false;
}
$thumb_data = self::createThumbnailData($file['tmp_name'], $file['type'], 300);
if (!$thumb_data) {
Session::add('feedback_negative', 'Failed to create thumbnail');
return false;
}
$encryption_key = bin2hex(random_bytes(32));
$iv = random_bytes(openssl_cipher_iv_length(self::$cipher));
$encrypted_image = openssl_encrypt($image_data, self::$cipher, $encryption_key, OPENSSL_RAW_DATA, $iv);
$encrypted_thumb = openssl_encrypt($thumb_data, self::$cipher, $encryption_key, OPENSSL_RAW_DATA, $iv);
$encrypted_image = base64_encode($iv . $encrypted_image);
$encrypted_thumb = base64_encode($iv . $encrypted_thumb);
$upload_dir = dirname(__FILE__) . '/../../public/gallery_uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$filename = uniqid('enc_') . '_' . time() . '.bin';
$thumb_filename = 'thumb_' . $filename;
if (file_put_contents($upload_dir . $filename, $encrypted_image) === false) {
Session::add('feedback_negative', 'Failed to save encrypted image');
return false;
}
if (file_put_contents($upload_dir . $thumb_filename, $encrypted_thumb) === false) {
unlink($upload_dir . $filename);
Session::add('feedback_negative', 'Failed to save encrypted thumbnail');
return false;
}
try {
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO gallery (user_id, filename, thumb_filename, title, description, is_public, file_size, mime_type, encryption_key)
VALUES (:user_id, :filename, :thumb_filename, :title, :description, :is_public, :file_size, :mime_type, :encryption_key)";
$query = $database->prepare($sql);
$result = $query->execute(array(
':user_id' => $user_id,
':filename' => $filename,
':thumb_filename' => $thumb_filename,
':title' => $title ?: pathinfo($file['name'], PATHINFO_FILENAME),
':description' => $description,
':is_public' => $is_public ? 1 : 0,
':file_size' => $file['size'],
':mime_type' => $file['type'],
':encryption_key' => $encryption_key
));
if ($result) {
return $database->lastInsertId();
}
unlink($upload_dir . $filename);
unlink($upload_dir . $thumb_filename);
Session::add('feedback_negative', 'Failed to save to database');
return false;
} catch (PDOException $e) {
unlink($upload_dir . $filename);
unlink($upload_dir . $thumb_filename);
Session::add('feedback_negative', 'Database error: ' . $e->getMessage());
return false;
}
}
public static function getDecryptedImage($image_id, $thumbnail = false)
{
$image = self::getImage($image_id);
if (!$image) {
return null;
}
$upload_dir = dirname(__FILE__) . '/../../public/gallery_uploads/';
$filename = $thumbnail ? $image->thumb_filename : $image->filename;
$filepath = $upload_dir . $filename;
if (!file_exists($filepath)) {
return null;
}
$encrypted_data = file_get_contents($filepath);
if ($encrypted_data === false) {
return null;
}
$encrypted_data = base64_decode($encrypted_data);
$iv_length = openssl_cipher_iv_length(self::$cipher);
$iv = substr($encrypted_data, 0, $iv_length);
$encrypted_data = substr($encrypted_data, $iv_length);
$decrypted = openssl_decrypt($encrypted_data, self::$cipher, $image->encryption_key, OPENSSL_RAW_DATA, $iv);
return array(
'data' => $decrypted,
'mime_type' => $image->mime_type
);
}
public static function updateImage($image_id, $title, $description, $is_public)
{
$user_id = Session::get('user_id');
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE gallery SET title = :title, description = :description, is_public = :is_public
WHERE id = :image_id AND user_id = :user_id";
$query = $database->prepare($sql);
return $query->execute(array(
':image_id' => $image_id,
':user_id' => $user_id,
':title' => $title,
':description' => $description,
':is_public' => $is_public ? 1 : 0
));
}
public static function deleteImage($image_id)
{
$user_id = Session::get('user_id');
$image = self::getImage($image_id);
if (!$image || $image->user_id != $user_id) {
return false;
}
$upload_dir = dirname(__FILE__) . '/../../public/gallery_uploads/';
$filepath = $upload_dir . $image->filename;
$thumbpath = $upload_dir . $image->thumb_filename;
if (file_exists($filepath)) {
unlink($filepath);
}
if (file_exists($thumbpath)) {
unlink($thumbpath);
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "DELETE FROM gallery WHERE id = :image_id AND user_id = :user_id";
$query = $database->prepare($sql);
return $query->execute(array(':image_id' => $image_id, ':user_id' => $user_id));
}
private static function createThumbnailData($source, $mime_type, $max_size)
{
$image_info = getimagesize($source);
if (!$image_info) {
return false;
}
$width = $image_info[0];
$height = $image_info[1];
$type = $image_info[2];
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($source);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($source);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($source);
break;
case IMAGETYPE_WEBP:
$image = imagecreatefromwebp($source);
break;
default:
return false;
}
if (!$image) {
return false;
}
$ratio = min($max_size / $width, $max_size / $height);
if ($ratio >= 1) {
$new_width = $width;
$new_height = $height;
} else {
$new_width = (int)($width * $ratio);
$new_height = (int)($height * $ratio);
}
$thumb = imagecreatetruecolor($new_width, $new_height);
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
$transparent = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
imagefilledrectangle($thumb, 0, 0, $new_width, $new_height, $transparent);
}
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
ob_start();
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumb, null, 85);
break;
case IMAGETYPE_PNG:
imagepng($thumb, null, 8);
break;
case IMAGETYPE_GIF:
imagegif($thumb);
break;
case IMAGETYPE_WEBP:
imagewebp($thumb, null, 85);
break;
}
$thumb_data = ob_get_clean();
return $thumb_data;
}
public static function formatFileSize($bytes)
{
if ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
}
return $bytes . ' bytes';
}
}

View File

@@ -76,7 +76,7 @@
</span>
<span class="tree-label"><?php echo htmlspecialchars($table); ?></span>
</div>
<div class="tree-children">
<div class="tree-children"<?php echo $is_current_table ? ' data-loaded="true"' : ''; ?>>
<?php if ($is_current_table && !empty($columns)): ?>
<?php foreach ($columns as $col): ?>
<div class="tree-item">

View File

@@ -8,6 +8,7 @@
<link rel="icon" href="data:;base64,=">
<!-- CSS -->
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/gallery.css" />
</head>
<body>
<!-- wrapper, to center website -->
@@ -27,6 +28,9 @@
<li <?php if (View::checkForActiveController($filename, "directory")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>directory/index">Benutzer</a>
</li>
<li <?php if (View::checkForActiveController($filename, "gallery")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>gallery/index">Gallery</a>
</li>
<?php if (Session::userIsLoggedIn()) { ?>
<li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a>

View File

@@ -0,0 +1,51 @@
<div class="gallery-container">
<div class="gallery-header">
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>" class="gallery-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to Image
</a>
</div>
<div class="gallery-form-container">
<h1>Edit Image</h1>
<div class="gallery-edit-preview">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($this->image->title); ?>">
</div>
<form method="post" action="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $this->image->id; ?>" class="gallery-form">
<div class="gallery-form-group">
<label class="gallery-form-label" for="title">Title</label>
<input type="text" name="title" id="title" class="gallery-form-input"
value="<?php echo htmlspecialchars($this->image->title); ?>" required>
</div>
<div class="gallery-form-group">
<label class="gallery-form-label" for="description">Description</label>
<textarea name="description" id="description" class="gallery-form-textarea" rows="4"><?php echo htmlspecialchars($this->image->description); ?></textarea>
</div>
<div class="gallery-form-group">
<label class="gallery-form-checkbox">
<input type="checkbox" name="is_public" value="1" <?php echo $this->image->is_public ? 'checked' : ''; ?>>
<span>Make this image public</span>
</label>
<small class="gallery-form-hint">Public images are visible to everyone in the gallery</small>
</div>
<div class="gallery-form-actions">
<button type="submit" name="submit_edit" class="gallery-btn gallery-btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Save Changes
</button>
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>" class="gallery-btn gallery-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,59 @@
<div class="gallery-container">
<div class="gallery-header">
<h1>Gallery</h1>
<div class="gallery-actions">
<?php if (Session::userIsLoggedIn()): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-btn gallery-btn-secondary">My Images</a>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary">Upload Image</a>
<?php endif; ?>
</div>
</div>
<?php if (empty($this->images)): ?>
<div class="gallery-empty">
<div class="gallery-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<h3>No images yet</h3>
<p>Be the first to share an image!</p>
<?php if (Session::userIsLoggedIn()): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary" style="margin-top: 16px;">Upload Image</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="gallery-grid">
<?php foreach ($this->images as $image): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $image->id; ?>" class="gallery-item">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($image->title); ?>"
loading="lazy">
<div class="gallery-item-overlay">
<span class="gallery-item-title"><?php echo htmlspecialchars($image->title); ?></span>
<span class="gallery-item-author">by <?php echo htmlspecialchars($image->user_name); ?></span>
</div>
</a>
<?php endforeach; ?>
</div>
<?php
$total_pages = ceil($this->total_images / $this->per_page);
if ($total_pages > 1):
?>
<div class="gallery-pagination">
<?php if ($this->current_page > 1): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/index/<?php echo $this->current_page - 1; ?>" class="gallery-btn gallery-btn-secondary">Previous</a>
<?php endif; ?>
<span class="gallery-pagination-info">Page <?php echo $this->current_page; ?> of <?php echo $total_pages; ?></span>
<?php if ($this->current_page < $total_pages): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/index/<?php echo $this->current_page + 1; ?>" class="gallery-btn gallery-btn-secondary">Next</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<div class="gallery-container">
<div class="gallery-header">
<h1>My Images</h1>
<div class="gallery-actions">
<a href="<?php echo Config::get('URL'); ?>gallery/index" class="gallery-btn gallery-btn-secondary">Public Gallery</a>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary">Upload Image</a>
</div>
</div>
<?php if (empty($this->images)): ?>
<div class="gallery-empty">
<div class="gallery-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<h3>No images uploaded</h3>
<p>Upload your first image to get started.</p>
<a href="<?php echo Config::get('URL'); ?>gallery/upload" class="gallery-btn gallery-btn-primary" style="margin-top: 16px;">Upload Image</a>
</div>
<?php else: ?>
<div class="gallery-grid">
<?php foreach ($this->images as $image): ?>
<div class="gallery-item gallery-item-owned">
<a href="<?php echo Config::get('URL'); ?>gallery/view/<?php echo $image->id; ?>">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($image->title); ?>"
loading="lazy">
</a>
<div class="gallery-item-overlay">
<span class="gallery-item-title"><?php echo htmlspecialchars($image->title); ?></span>
<span class="gallery-item-visibility <?php echo $image->is_public ? 'public' : 'private'; ?>">
<?php echo $image->is_public ? 'Public' : 'Private'; ?>
</span>
</div>
<div class="gallery-item-actions">
<a href="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $image->id; ?>" class="gallery-btn-icon" title="Edit">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</a>
<a href="<?php echo Config::get('URL'); ?>gallery/delete/<?php echo $image->id; ?>"
class="gallery-btn-icon gallery-btn-danger"
title="Delete"
onclick="return confirm('Delete this image?');">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
$total_pages = ceil($this->total_images / $this->per_page);
if ($total_pages > 1):
?>
<div class="gallery-pagination">
<?php if ($this->current_page > 1): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/my/<?php echo $this->current_page - 1; ?>" class="gallery-btn gallery-btn-secondary">Previous</a>
<?php endif; ?>
<span class="gallery-pagination-info">Page <?php echo $this->current_page; ?> of <?php echo $total_pages; ?></span>
<?php if ($this->current_page < $total_pages): ?>
<a href="<?php echo Config::get('URL'); ?>gallery/my/<?php echo $this->current_page + 1; ?>" class="gallery-btn gallery-btn-secondary">Next</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,168 @@
<div class="gallery-success-overlay">
<div class="gallery-success-card">
<div class="gallery-success-icon">
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
</div>
<h2>Upload Successful!</h2>
<p>Your image has been uploaded successfully.</p>
<div class="gallery-success-preview">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/thumb"
alt="<?php echo htmlspecialchars($this->image->title); ?>">
</div>
<p class="gallery-success-title"><?php echo htmlspecialchars($this->image->title); ?></p>
<p class="gallery-success-redirect">Redirecting to your image<span class="dots"></span></p>
</div>
</div>
<style>
.gallery-success-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.98);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.gallery-success-card {
text-align: center;
padding: 40px;
max-width: 400px;
animation: slideUp 0.4s ease 0.1s both;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.gallery-success-icon {
margin-bottom: 24px;
}
.checkmark {
width: 80px;
height: 80px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #4CAF50;
stroke-miterlimit: 10;
margin: 0 auto;
box-shadow: inset 0px 0px 0px #4CAF50;
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
}
.checkmark-circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #4CAF50;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark-check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% { stroke-dashoffset: 0; }
}
@keyframes scale {
0%, 100% { transform: none; }
50% { transform: scale3d(1.1, 1.1, 1); }
}
@keyframes fill {
100% { box-shadow: inset 0px 0px 0px 40px rgba(76, 175, 80, 0.1); }
}
.gallery-success-card h2 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: #333;
}
.gallery-success-card p {
margin: 0;
color: #666;
font-size: 16px;
}
.gallery-success-preview {
margin: 24px 0 16px;
animation: previewFade 0.5s ease 0.6s both;
}
@keyframes previewFade {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.gallery-success-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
object-fit: cover;
}
.gallery-success-title {
font-weight: 500;
color: #333;
margin-bottom: 24px !important;
}
.gallery-success-redirect {
font-size: 14px !important;
color: #999 !important;
animation: pulse 1.5s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.dots::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
</style>
<script>
setTimeout(function() {
window.location.href = '<?php echo Config::get('URL'); ?>gallery/view/<?php echo $this->image->id; ?>';
}, 2500);
</script>

View File

@@ -0,0 +1,437 @@
<div class="gallery-container">
<div class="gallery-header">
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to My Images
</a>
</div>
<div class="gallery-form-container">
<h1>Upload Image</h1>
<?php $this->renderFeedbackMessages(); ?>
<form id="upload-form" method="post" action="<?php echo Config::get('URL'); ?>gallery/upload" enctype="multipart/form-data" class="gallery-form">
<div class="gallery-form-group">
<label class="gallery-form-label">Image File</label>
<div class="gallery-upload-zone" id="upload-zone">
<input type="file" name="image" id="image-input" accept="image/jpeg,image/png,image/gif,image/webp" required>
<div class="gallery-upload-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span>Click or drag image here</span>
<small>JPG, PNG, GIF, WebP - Max 10MB</small>
</div>
<div class="gallery-upload-preview" id="upload-preview" style="display: none;">
<img id="preview-image" src="" alt="Preview">
<button type="button" class="gallery-upload-remove" id="remove-preview">&times;</button>
</div>
</div>
</div>
<div class="gallery-form-group">
<label class="gallery-form-label" for="title">Title</label>
<input type="text" name="title" id="title" class="gallery-form-input" placeholder="Give your image a title">
</div>
<div class="gallery-form-group">
<label class="gallery-form-label" for="description">Description</label>
<textarea name="description" id="description" class="gallery-form-textarea" rows="4" placeholder="Add a description (optional)"></textarea>
</div>
<div class="gallery-form-group">
<label class="gallery-form-checkbox">
<input type="checkbox" name="is_public" value="1" checked>
<span>Make this image public</span>
</label>
<small class="gallery-form-hint">Public images are visible to everyone in the gallery</small>
</div>
<div class="gallery-form-actions">
<button type="submit" name="submit_upload" class="gallery-btn gallery-btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Upload Image
</button>
<a href="<?php echo Config::get('URL'); ?>gallery/my" class="gallery-btn gallery-btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<!-- Upload Progress Overlay -->
<div id="upload-overlay" class="upload-overlay" style="display: none;">
<div class="upload-modal">
<!-- Progress State -->
<div id="upload-progress-state" class="upload-state">
<div class="upload-icon uploading">
<svg class="upload-spinner" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
</div>
<h2>Uploading...</h2>
<div class="upload-progress-bar">
<div class="upload-progress-fill" id="progress-fill"></div>
</div>
<p class="upload-progress-text"><span id="progress-percent">0</span>%</p>
</div>
<!-- Success State -->
<div id="upload-success-state" class="upload-state" style="display: none;">
<div class="upload-icon success">
<svg class="checkmark" viewBox="0 0 52 52">
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
</div>
<h2>Upload Complete!</h2>
<p>Your image has been uploaded successfully.</p>
<p class="upload-redirect">Redirecting<span class="dots"></span></p>
</div>
<!-- Error State -->
<div id="upload-error-state" class="upload-state" style="display: none;">
<div class="upload-icon error">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
</div>
<h2>Upload Failed</h2>
<p id="error-message">Something went wrong. Please try again.</p>
<button type="button" class="gallery-btn gallery-btn-primary" onclick="hideOverlay()">Try Again</button>
</div>
</div>
</div>
<style>
.upload-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.upload-modal {
text-align: center;
padding: 40px;
max-width: 400px;
}
.upload-state {
animation: slideUp 0.4s ease;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.upload-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
}
.upload-icon svg {
width: 100%;
height: 100%;
}
.upload-icon.uploading svg {
stroke: #333;
}
.upload-icon.success svg {
stroke: #4CAF50;
}
.upload-icon.error svg {
stroke: #dc3545;
}
.upload-spinner {
animation: rotate 1.5s linear infinite;
}
.upload-spinner circle {
stroke: #333;
stroke-linecap: round;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite;
}
@keyframes rotate {
100% { transform: rotate(360deg); }
}
@keyframes dash {
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
}
.upload-modal h2 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
color: #333;
}
.upload-modal p {
margin: 0;
color: #666;
font-size: 14px;
}
.upload-progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0 12px;
}
.upload-progress-fill {
height: 100%;
background: linear-gradient(90deg, #333, #555);
border-radius: 4px;
width: 0%;
transition: width 0.3s ease;
}
.upload-progress-text {
font-family: monospace;
font-size: 16px !important;
font-weight: 600;
color: #333 !important;
}
/* Checkmark Animation */
.checkmark {
width: 80px;
height: 80px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #4CAF50;
stroke-miterlimit: 10;
box-shadow: inset 0px 0px 0px #4CAF50;
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
}
.checkmark-circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #4CAF50;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark-check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% { stroke-dashoffset: 0; }
}
@keyframes scale {
0%, 100% { transform: none; }
50% { transform: scale3d(1.1, 1.1, 1); }
}
@keyframes fill {
100% { box-shadow: inset 0px 0px 0px 40px rgba(76, 175, 80, 0.1); }
}
.upload-redirect {
margin-top: 20px !important;
font-size: 13px !important;
color: #999 !important;
animation: pulse 1.5s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.dots::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('upload-form');
const uploadZone = document.getElementById('upload-zone');
const imageInput = document.getElementById('image-input');
const uploadPreview = document.getElementById('upload-preview');
const previewImage = document.getElementById('preview-image');
const removePreview = document.getElementById('remove-preview');
const placeholder = uploadZone.querySelector('.gallery-upload-placeholder');
const overlay = document.getElementById('upload-overlay');
const progressState = document.getElementById('upload-progress-state');
const successState = document.getElementById('upload-success-state');
const errorState = document.getElementById('upload-error-state');
const progressFill = document.getElementById('progress-fill');
const progressPercent = document.getElementById('progress-percent');
const errorMessage = document.getElementById('error-message');
// Image preview
imageInput.addEventListener('change', function(e) {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
previewImage.src = e.target.result;
placeholder.style.display = 'none';
uploadPreview.style.display = 'block';
};
reader.readAsDataURL(this.files[0]);
}
});
removePreview.addEventListener('click', function() {
imageInput.value = '';
previewImage.src = '';
placeholder.style.display = 'flex';
uploadPreview.style.display = 'none';
});
// Drag and drop
uploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('dragover');
});
uploadZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
imageInput.files = e.dataTransfer.files;
imageInput.dispatchEvent(new Event('change'));
}
});
// Form submission with AJAX
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!imageInput.files || !imageInput.files[0]) {
alert('Please select an image to upload');
return;
}
const formData = new FormData(form);
formData.append('submit_upload', '1');
// Show overlay with progress state
showOverlay('progress');
const xhr = new XMLHttpRequest();
// Progress tracking
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressFill.style.width = percent + '%';
progressPercent.textContent = percent;
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
showOverlay('success');
// Redirect after animation
setTimeout(function() {
window.location.href = '<?php echo Config::get('URL'); ?>gallery/view/' + response.image_id;
}, 2000);
} else {
showOverlay('error', response.message || 'Upload failed');
}
} catch (e) {
showOverlay('error', 'Invalid server response');
}
} else {
showOverlay('error', 'Server error: ' + xhr.status);
}
});
xhr.addEventListener('error', function() {
showOverlay('error', 'Network error. Please check your connection.');
});
xhr.open('POST', form.action);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send(formData);
});
function showOverlay(state, message) {
overlay.style.display = 'flex';
progressState.style.display = 'none';
successState.style.display = 'none';
errorState.style.display = 'none';
if (state === 'progress') {
progressState.style.display = 'block';
progressFill.style.width = '0%';
progressPercent.textContent = '0';
} else if (state === 'success') {
successState.style.display = 'block';
} else if (state === 'error') {
errorState.style.display = 'block';
if (message) {
errorMessage.textContent = message;
}
}
}
window.hideOverlay = function() {
overlay.style.display = 'none';
};
});
</script>

View File

@@ -0,0 +1,80 @@
<div class="gallery-container">
<div class="gallery-header">
<a href="<?php echo Config::get('URL'); ?>gallery/index" class="gallery-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to Gallery
</a>
</div>
<div class="gallery-view">
<div class="gallery-view-image">
<img src="<?php echo Config::get('URL'); ?>gallery/image/<?php echo $this->image->id; ?>/full"
alt="<?php echo htmlspecialchars($this->image->title); ?>">
</div>
<div class="gallery-view-info">
<h1><?php echo htmlspecialchars($this->image->title); ?></h1>
<div class="gallery-view-meta">
<div class="gallery-view-author">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<a href="<?php echo Config::get('URL'); ?>profile/showProfile/<?php echo $this->image->user_name; ?>">
<?php echo htmlspecialchars($this->image->user_name); ?>
</a>
</div>
<div class="gallery-view-date">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
<?php echo date('M j, Y', strtotime($this->image->created_at)); ?>
</div>
<div class="gallery-view-size">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<?php echo GalleryModel::formatFileSize($this->image->file_size); ?>
</div>
</div>
<?php if ($this->image->description): ?>
<div class="gallery-view-description">
<?php echo nl2br(htmlspecialchars($this->image->description)); ?>
</div>
<?php endif; ?>
<?php if ($this->image->user_id == Session::get('user_id')): ?>
<div class="gallery-view-actions">
<a href="<?php echo Config::get('URL'); ?>gallery/edit/<?php echo $this->image->id; ?>" class="gallery-btn gallery-btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
Edit
</a>
<a href="<?php echo Config::get('URL'); ?>gallery/delete/<?php echo $this->image->id; ?>"
class="gallery-btn gallery-btn-danger"
onclick="return confirm('Delete this image permanently?');">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -1,67 +1,66 @@
<div class="dbmanager-container">
<div class="dbmanager-sidebar">
<h3>Databases</h3>
<ul class="db-tree">
<?php foreach ($this->databases as $db): ?>
<li class="db-item <?php echo ($db === $this->database_name) ? 'active' : ''; ?>">
<a href="<?php echo Config::get('URL'); ?>sql/index/<?php echo urlencode($db); ?>" class="db-link">
<?php echo htmlspecialchars($db); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<div class="db-actions">
<a href="<?php echo Config::get('URL'); ?>database/index" class="btn" style="display:block;text-align:center;">Back to Databases</a>
<div class="dbm-content-header">
<div class="dbm-breadcrumb">
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
<span class="separator">/</span>
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
<span class="separator">/</span>
<span>SQL Console</span>
</div>
<?php if (!empty($this->history)): ?>
<div style="margin-top:20px;">
<h4>Query History</h4>
<ul class="query-history">
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
<li class="history-item" onclick="document.getElementById('sql_query').value = this.dataset.query;" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
<?php echo htmlspecialchars(substr($item['query_text'], 0, 40)); ?>...
</li>
<?php endforeach; ?>
</ul>
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="btn btn-small btn-danger" style="margin-top:10px;">Clear History</a>
</div>
<?php endif; ?>
<div class="dbm-title">
<h1>SQL Console</h1>
<span class="badge"><?php echo htmlspecialchars($this->database_name); ?></span>
</div>
</div>
<div class="dbmanager-content">
<h2>SQL Console</h2>
<p>Database: <strong><?php echo htmlspecialchars($this->database_name); ?></strong></p>
<div class="dbm-content-body">
<div class="sql-console">
<form method="post" action="<?php echo Config::get('URL'); ?>sql/execute" id="sql-form">
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($this->database_name); ?>">
<div class="sql-editor">
<textarea name="sql_query" id="sql_query" rows="6" placeholder="Enter your SQL query here...
Example: SELECT * FROM users LIMIT 10"></textarea>
<div class="sql-editor-container">
<div class="sql-editor-wrapper">
<pre class="sql-highlight" id="sql-highlight" aria-hidden="true"></pre>
<textarea name="sql_query" id="sql_query" class="sql-textarea" spellcheck="false" placeholder="SELECT * FROM users LIMIT 10;"><?php echo isset($_POST['sql_query']) ? htmlspecialchars($_POST['sql_query']) : ''; ?></textarea>
</div>
<div class="sql-line-numbers" id="line-numbers">1</div>
</div>
<div class="sql-actions">
<button type="submit" class="btn">Execute Query</button>
<button type="button" class="btn" style="background:#6c757d;" onclick="document.getElementById('sql_query').value = '';">Clear</button>
<div class="sql-toolbar">
<div class="sql-toolbar-left">
<button type="submit" class="dbm-btn dbm-btn-success">
<i data-lucide="play"></i>
Execute
</button>
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="formatSQL()">
<i data-lucide="align-left"></i>
Format
</button>
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="clearSQL()">
<i data-lucide="trash-2"></i>
Clear
</button>
</div>
<div class="sql-toolbar-right">
<span class="sql-hint">Ctrl+Enter to execute</span>
</div>
</div>
</form>
<div id="sql-result" class="sql-result">
<?php
// Check for session result
$result = Session::get('sql_result');
if ($result) {
Session::set('sql_result', null);
if ($result['success']) {
echo '<div class="result-success">';
echo '<p>' . htmlspecialchars($result['message']) . '</p>';
echo '<p class="execution-time">Execution time: ' . $result['execution_time'] . ' ms</p>';
echo '<div class="sql-result-success">';
echo '<div class="sql-result-header">';
echo '<span class="sql-result-status"><i data-lucide="check-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
echo '<span class="sql-result-time">' . $result['execution_time'] . ' ms</span>';
echo '</div>';
if (!empty($result['result'])) {
echo '<div class="table-wrapper"><table class="data-table"><thead><tr>';
echo '<div class="sql-result-table-wrapper"><table class="dbm-table"><thead><tr>';
foreach (array_keys($result['result'][0]) as $col) {
echo '<th>' . htmlspecialchars($col) . '</th>';
}
@@ -69,7 +68,7 @@ Example: SELECT * FROM users LIMIT 10"></textarea>
foreach ($result['result'] as $row) {
echo '<tr>';
foreach ($row as $value) {
echo '<td>' . ($value === null ? '<span style="color:#999;">NULL</span>' : htmlspecialchars(substr($value, 0, 100))) . '</td>';
echo '<td>' . ($value === null ? '<span class="null-value">NULL</span>' : htmlspecialchars(substr($value, 0, 200))) . '</td>';
}
echo '</tr>';
}
@@ -77,206 +76,398 @@ Example: SELECT * FROM users LIMIT 10"></textarea>
}
echo '</div>';
} else {
echo '<div class="result-error">';
echo '<p><strong>Error:</strong> ' . htmlspecialchars($result['message']) . '</p>';
echo '<div class="sql-result-error">';
echo '<div class="sql-result-header">';
echo '<span class="sql-result-status"><i data-lucide="x-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
echo '</div>';
if (!empty($result['error'])) {
echo '<p class="error-details">' . htmlspecialchars($result['error']) . '</p>';
echo '<pre class="sql-error-details">' . htmlspecialchars($result['error']) . '</pre>';
}
echo '</div>';
}
}
?>
</div>
<?php if (!empty($this->history)): ?>
<div class="sql-history">
<h3>Recent Queries</h3>
<div class="sql-history-list">
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
<div class="sql-history-item" onclick="loadQuery(this)" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
<code><?php echo htmlspecialchars(substr($item['query_text'], 0, 80)); ?><?php echo strlen($item['query_text']) > 80 ? '...' : ''; ?></code>
<span class="sql-history-time"><?php echo date('M j, H:i', strtotime($item['query_timestamp'])); ?></span>
</div>
<?php endforeach; ?>
</div>
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="dbm-btn dbm-btn-sm dbm-btn-secondary" style="margin-top: 12px;">Clear History</a>
</div>
<?php endif; ?>
</div>
</div>
<style>
.dbmanager-container {
.sql-console {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
}
.dbmanager-sidebar {
width: 250px;
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
flex-shrink: 0;
.sql-editor-container {
display: flex;
border: 1px solid var(--dbm-border);
border-radius: var(--dbm-radius);
overflow: hidden;
background: #1e1e1e;
}
.dbmanager-sidebar h3, .dbmanager-sidebar h4 {
margin: 0 0 10px 0;
.sql-line-numbers {
padding: 12px 8px;
background: #252526;
color: #858585;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
color: #333;
line-height: 1.5;
text-align: right;
user-select: none;
min-width: 40px;
border-right: 1px solid #333;
}
.db-tree {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
max-height: 200px;
overflow-y: auto;
}
.db-item {
padding: 5px 10px;
border-radius: 3px;
margin-bottom: 2px;
}
.db-item:hover {
background: #e0e0e0;
}
.db-item.active {
background: #007bff;
}
.db-item.active .db-link {
color: #fff;
}
.db-link {
text-decoration: none;
color: #333;
display: block;
}
.query-history {
list-style: none;
padding: 0;
margin: 0;
max-height: 200px;
overflow-y: auto;
}
.history-item {
padding: 5px;
font-size: 11px;
color: #666;
cursor: pointer;
border-bottom: 1px solid #ddd;
word-break: break-all;
}
.history-item:hover {
background: #e0e0e0;
}
.db-actions {
border-top: 1px solid #ddd;
padding-top: 15px;
}
.dbmanager-content {
.sql-editor-wrapper {
flex: 1;
position: relative;
min-height: 180px;
}
.dbmanager-content h2 {
margin-top: 0;
}
.sql-editor textarea {
width: 100%;
padding: 12px;
font-family: 'Consolas', 'Monaco', monospace;
.sql-highlight, .sql-textarea {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 5px;
resize: vertical;
line-height: 1.5;
padding: 12px;
margin: 0;
border: none;
width: 100%;
min-height: 180px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.sql-highlight {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
background: transparent;
color: #d4d4d4;
overflow: hidden;
}
.sql-textarea {
position: relative;
background: transparent;
color: transparent;
caret-color: #fff;
resize: none;
outline: none;
z-index: 1;
box-sizing: border-box;
}
.sql-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
.sql-result {
margin-top: 20px;
}
.result-success {
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 5px;
padding: 15px;
}
.result-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 5px;
padding: 15px;
}
.execution-time {
font-size: 12px;
.sql-textarea::placeholder {
color: #666;
}
.error-details {
/* Syntax highlighting colors - VS Code dark theme */
.sql-highlight .sql-keyword { color: #569cd6 !important; font-weight: bold !important; }
.sql-highlight .sql-function { color: #dcdcaa !important; }
.sql-highlight .sql-string { color: #ce9178 !important; }
.sql-highlight .sql-number { color: #b5cea8 !important; }
.sql-highlight .sql-operator { color: #d4d4d4 !important; }
.sql-highlight .sql-comment { color: #6a9955 !important; font-style: italic !important; }
.sql-highlight .sql-table { color: #4ec9b0 !important; }
.sql-highlight .sql-column { color: #9cdcfe !important; }
.sql-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.sql-toolbar-left {
display: flex;
gap: 8px;
}
.sql-hint {
font-size: 12px;
color: var(--dbm-text-muted);
}
.sql-result {
margin-top: 8px;
}
.sql-result-success {
background: #f0f9f0;
border: 1px solid #c3e6c3;
border-radius: var(--dbm-radius);
overflow: hidden;
}
.sql-result-error {
background: #fdf0f0;
border: 1px solid #f5c6c6;
border-radius: var(--dbm-radius);
overflow: hidden;
}
.sql-result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(0,0,0,0.03);
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.sql-result-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
}
.sql-result-success .sql-result-status { color: #2e7d32; }
.sql-result-error .sql-result-status { color: #c62828; }
.sql-result-time {
font-size: 12px;
color: #666;
font-family: monospace;
}
.sql-result-table-wrapper {
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
.sql-result-table-wrapper .dbm-table {
margin: 0;
font-size: 12px;
}
.sql-result-table-wrapper .dbm-table th {
position: sticky;
top: 0;
background: #f8f9fa;
z-index: 1;
}
.null-value {
color: #999;
font-style: italic;
}
.sql-error-details {
margin: 0;
padding: 12px 16px;
font-family: monospace;
font-size: 12px;
color: #721c24;
color: #c62828;
white-space: pre-wrap;
word-break: break-all;
}
.table-wrapper {
overflow-x: auto;
margin-top: 15px;
.sql-history {
background: var(--dbm-bg-secondary);
border-radius: var(--dbm-radius);
padding: 16px;
}
.data-table {
width: 100%;
border-collapse: collapse;
.sql-history h3 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: var(--dbm-text);
}
.sql-history-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.sql-history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: var(--dbm-bg);
border-radius: var(--dbm-radius);
cursor: pointer;
transition: background 0.15s;
}
.sql-history-item:hover {
background: var(--dbm-bg-tertiary);
}
.sql-history-item code {
font-size: 12px;
background: #fff;
}
.data-table th,
.data-table td {
padding: 8px;
text-align: left;
border: 1px solid #ddd;
max-width: 200px;
color: var(--dbm-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 12px;
}
.sql-history-time {
font-size: 11px;
color: var(--dbm-text-muted);
white-space: nowrap;
}
.data-table th {
background: #f8f9fa;
font-weight: bold;
}
.btn {
display: inline-block;
padding: 8px 16px;
text-decoration: none;
border-radius: 3px;
font-size: 14px;
cursor: pointer;
border: none;
background: #007bff;
color: #fff;
}
.btn:hover {
background: #0056b3;
}
.btn-small {
padding: 4px 8px;
font-size: 11px;
}
.btn-danger {
background: #dc3545;
}
.btn-danger:hover {
background: #c82333;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const textarea = document.getElementById('sql_query');
const highlight = document.getElementById('sql-highlight');
const lineNumbers = document.getElementById('line-numbers');
function updateHighlight() {
const code = textarea.value;
highlight.innerHTML = highlightSQL(code) + '\n';
updateLineNumbers();
}
function updateLineNumbers() {
const lines = textarea.value.split('\n').length;
let nums = '';
for (let i = 1; i <= lines; i++) {
nums += i + '\n';
}
lineNumbers.textContent = nums;
}
function highlightSQL(code) {
if (!code) return '';
// Escape HTML
code = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Use inline styles for guaranteed coloring
const styles = {
keyword: 'color:#569cd6;font-weight:bold',
function: 'color:#dcdcaa',
string: 'color:#ce9178',
number: 'color:#b5cea8',
comment: 'color:#6a9955;font-style:italic'
};
// Comments (must be first to avoid highlighting keywords inside comments)
code = code.replace(/(--[^\n]*)/g, '<span style="' + styles.comment + '">$1</span>');
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span style="' + styles.comment + '">$1</span>');
// Strings
code = code.replace(/('[^']*')/g, '<span style="' + styles.string + '">$1</span>');
code = code.replace(/("[^"]*")/g, '<span style="' + styles.string + '">$1</span>');
// Numbers (but not inside already-styled spans)
code = code.replace(/\b(\d+\.?\d*)\b(?![^<]*>)/g, '<span style="' + styles.number + '">$1</span>');
// Keywords
const keywords = [
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'NULL',
'ORDER', 'BY', 'ASC', 'DESC', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
'CREATE', 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'TRIGGER', 'PROCEDURE', 'FUNCTION',
'ALTER', 'DROP', 'TRUNCATE', 'ADD', 'MODIFY', 'CHANGE', 'RENAME',
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'OUTER', 'CROSS', 'ON', 'USING',
'UNION', 'ALL', 'DISTINCT', 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'DEFAULT',
'AUTO_INCREMENT', 'ENGINE', 'CHARSET', 'COLLATE',
'IF', 'EXISTS', 'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE',
'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION'
];
const keywordRegex = new RegExp('\\b(' + keywords.join('|') + ')\\b(?![^<]*>)', 'gi');
code = code.replace(keywordRegex, '<span style="' + styles.keyword + '">$1</span>');
// Functions
const functions = [
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH', 'UPPER', 'LOWER',
'TRIM', 'LTRIM', 'RTRIM', 'REPLACE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST', 'CONVERT',
'DATE', 'NOW', 'CURDATE', 'CURTIME', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND',
'DATE_FORMAT', 'DATEDIFF', 'DATE_ADD', 'DATE_SUB', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD',
'RAND', 'UUID', 'MD5', 'SHA1', 'SHA2', 'GROUP_CONCAT', 'JSON_OBJECT', 'JSON_ARRAY'
];
const funcRegex = new RegExp('\\b(' + functions.join('|') + ')\\s*\\(', 'gi');
code = code.replace(funcRegex, '<span style="' + styles.function + '">$1</span>(');
return code;
}
textarea.addEventListener('input', updateHighlight);
textarea.addEventListener('scroll', function() {
highlight.scrollTop = textarea.scrollTop;
highlight.scrollLeft = textarea.scrollLeft;
});
// Ctrl+Enter to execute
textarea.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
document.getElementById('sql-form').submit();
}
// Tab to insert spaces
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
updateHighlight();
}
});
updateHighlight();
lucide.createIcons();
});
function loadQuery(element) {
document.getElementById('sql_query').value = element.dataset.query;
document.getElementById('sql_query').dispatchEvent(new Event('input'));
document.getElementById('sql_query').focus();
}
function clearSQL() {
document.getElementById('sql_query').value = '';
document.getElementById('sql_query').dispatchEvent(new Event('input'));
}
function formatSQL() {
const textarea = document.getElementById('sql_query');
let sql = textarea.value;
// Basic formatting
const keywords = ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'ON', 'SET', 'VALUES', 'INSERT INTO', 'UPDATE', 'DELETE FROM'];
keywords.forEach(kw => {
const regex = new RegExp('\\b' + kw.replace(' ', '\\s+') + '\\b', 'gi');
sql = sql.replace(regex, '\n' + kw);
});
sql = sql.trim();
textarea.value = sql;
textarea.dispatchEvent(new Event('input'));
}
</script>

BIN
assets/screenshots/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
assets/screenshots/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/screenshots/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -124,28 +124,41 @@
background: var(--dbm-bg-tertiary);
}
.tree-header:hover .tree-toggle {
background: rgba(0, 0, 0, 0.06);
}
.tree-header.active {
background: var(--dbm-bg-tertiary);
border-left-color: var(--dbm-accent);
}
.tree-toggle {
width: 16px;
height: 16px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
margin-right: 2px;
margin-left: -4px;
color: var(--dbm-text-muted);
transition: var(--dbm-transition);
flex-shrink: 0;
border-radius: 3px;
cursor: pointer;
}
.tree-toggle:hover {
background: var(--dbm-bg-tertiary);
color: var(--dbm-text);
}
.tree-toggle svg,
.tree-toggle [data-lucide] {
width: 10px;
height: 10px;
width: 12px;
height: 12px;
transition: transform 0.15s ease;
pointer-events: none;
}
.tree-item.expanded > .tree-header .tree-toggle svg,
@@ -505,12 +518,7 @@ a.tree-label:hover {
overflow: hidden;
}
.sql-keyword { color: #555; font-weight: 600; }
.sql-function { color: #666; }
.sql-string { color: #888; }
.sql-number { color: #777; }
.sql-operator { color: #444; }
.sql-comment { color: #aaa; font-style: italic; }
/* SQL highlighting colors moved to sql/index.php view */
.dbm-sql-actions {
display: flex;

474
public/css/gallery.css Normal file
View File

@@ -0,0 +1,474 @@
.gallery-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.gallery-header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.gallery-actions {
display: flex;
gap: 8px;
}
.gallery-back {
display: inline-flex;
align-items: center;
gap: 8px;
color: #666;
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.gallery-back:hover {
color: #333;
}
.gallery-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
}
.gallery-btn-primary {
background: #333;
color: #fff;
}
.gallery-btn-primary:hover {
background: #555;
}
.gallery-btn-secondary {
background: #f0f0f0;
color: #333;
}
.gallery-btn-secondary:hover {
background: #e0e0e0;
}
.gallery-btn-danger {
background: #dc3545;
color: #fff;
}
.gallery-btn-danger:hover {
background: #c82333;
}
.gallery-btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
color: #666;
text-decoration: none;
transition: all 0.2s;
}
.gallery-btn-icon:hover {
background: #fff;
color: #333;
}
.gallery-btn-icon.gallery-btn-danger:hover {
background: #dc3545;
color: #fff;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.gallery-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
background: #f0f0f0;
text-decoration: none;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.05);
}
.gallery-item-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: #fff;
opacity: 0;
transition: opacity 0.3s;
}
.gallery-item:hover .gallery-item-overlay {
opacity: 1;
}
.gallery-item-title {
display: block;
font-weight: 500;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-item-author {
display: block;
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
}
.gallery-item-visibility {
display: inline-block;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-top: 4px;
}
.gallery-item-visibility.public {
background: rgba(40, 167, 69, 0.8);
}
.gallery-item-visibility.private {
background: rgba(255, 193, 7, 0.8);
color: #333;
}
.gallery-item-owned {
position: relative;
}
.gallery-item-owned .gallery-item-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.gallery-item-owned:hover .gallery-item-actions {
opacity: 1;
}
.gallery-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.gallery-empty-icon {
color: #ccc;
margin-bottom: 16px;
}
.gallery-empty h3 {
margin: 0 0 8px;
font-size: 18px;
color: #333;
}
.gallery-empty p {
margin: 0;
color: #666;
}
.gallery-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
}
.gallery-pagination-info {
font-size: 14px;
color: #666;
}
.gallery-view {
display: grid;
grid-template-columns: 1fr 350px;
gap: 32px;
margin-top: 24px;
}
@media (max-width: 900px) {
.gallery-view {
grid-template-columns: 1fr;
}
}
.gallery-view-image {
background: #f0f0f0;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.gallery-view-image img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.gallery-view-info h1 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
color: #333;
}
.gallery-view-meta {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 16px;
}
.gallery-view-author,
.gallery-view-date,
.gallery-view-size {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.gallery-view-author a {
color: #333;
text-decoration: none;
}
.gallery-view-author a:hover {
text-decoration: underline;
}
.gallery-view-description {
font-size: 14px;
line-height: 1.6;
color: #555;
margin-bottom: 24px;
}
.gallery-view-actions {
display: flex;
gap: 8px;
}
.gallery-form-container {
max-width: 600px;
margin: 0 auto;
}
.gallery-form-container h1 {
margin: 0 0 24px;
font-size: 24px;
font-weight: 600;
color: #333;
}
.gallery-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.gallery-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.gallery-form-label {
font-size: 14px;
font-weight: 500;
color: #333;
}
.gallery-form-input,
.gallery-form-textarea {
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.gallery-form-input:focus,
.gallery-form-textarea:focus {
outline: none;
border-color: #333;
}
.gallery-form-textarea {
resize: vertical;
min-height: 100px;
}
.gallery-form-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.gallery-form-checkbox input {
width: 18px;
height: 18px;
cursor: pointer;
}
.gallery-form-hint {
font-size: 12px;
color: #888;
}
.gallery-form-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.gallery-upload-zone {
position: relative;
border: 2px dashed #ddd;
border-radius: 8px;
transition: all 0.2s;
min-height: 200px;
}
.gallery-upload-zone:hover,
.gallery-upload-zone.dragover {
border-color: #333;
background: #fafafa;
}
.gallery-upload-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.gallery-upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #888;
text-align: center;
gap: 8px;
}
.gallery-upload-placeholder small {
font-size: 12px;
color: #aaa;
}
.gallery-upload-preview {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.gallery-upload-preview img {
max-width: 100%;
max-height: 300px;
border-radius: 4px;
}
.gallery-upload-remove {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
background: #333;
color: #fff;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.gallery-upload-remove:hover {
background: #dc3545;
}
.gallery-edit-preview {
text-align: center;
margin-bottom: 24px;
}
.gallery-edit-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
object-fit: cover;
}

View File

@@ -0,0 +1,12 @@
# Deny access to all files except images
<FilesMatch "\.(?!(jpg|jpeg|png|gif|webp)$)[^.]+$">
Deny from all
</FilesMatch>
# Disable directory listing
Options -Indexes
# Disable script execution
<FilesMatch "\.(php|php5|phtml|cgi|pl|py)$">
Deny from all
</FilesMatch>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,27 +11,40 @@ const DBManager = {
initTree() {
document.querySelectorAll('.tree-header').forEach(header => {
// Skip if already initialized
if (header.dataset.treeInit) return;
header.dataset.treeInit = 'true';
header.addEventListener('click', (e) => {
const item = header.closest('.tree-item');
const children = item.querySelector('.tree-children');
const href = header.dataset.href;
const toggle = e.target.closest('.tree-toggle');
// If clicking on toggle icon, just expand/collapse
// If clicking on toggle icon, just expand/collapse (don't navigate)
if (toggle) {
e.preventDefault();
e.stopPropagation();
// Lazy-load tables for databases if needed
if (item.dataset.db && children && !children.dataset.loaded) {
this.loadTables(item.dataset.db, children);
}
// Lazy-load columns for tables if needed
else if (item.dataset.table && children && !children.dataset.loaded) {
// Find the parent database name
const dbItem = item.closest('.tree-item[data-db]');
if (dbItem) {
this.loadColumns(dbItem.dataset.db, item.dataset.table, children);
}
}
this.toggleTreeItem(item);
return;
}
// If it's a table or database item with href, navigate directly
// If it's a database or table item with href, navigate
if (href) {
// For databases, also load tables if not loaded
// For databases, also expand and load tables
if (item.dataset.db && children && !children.dataset.loaded) {
this.loadTables(item.dataset.db, children);
}
@@ -68,6 +81,37 @@ const DBManager = {
}
},
async loadColumns(dbName, tableName, container) {
container.innerHTML = '<div class="tree-loading">Loading...</div>';
container.dataset.loaded = 'true';
try {
const response = await fetch(`${this.baseUrl}database/getColumns/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}`);
const data = await response.json();
if (data.success && data.columns) {
let html = '';
data.columns.forEach(col => {
html += `
<div class="tree-item">
<div class="tree-header">
<span class="tree-icon ${col.Key === 'PRI' ? 'key' : 'column'}">
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
</span>
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
</div>
</div>
`;
});
container.innerHTML = html || '<div class="tree-empty">No columns</div>';
this.refreshIcons();
}
} catch (error) {
container.innerHTML = '<div class="tree-error">Failed to load</div>';
}
},
renderTableTreeItem(dbName, tableName, columns) {
const columnsHtml = columns.map(col => `
<div class="tree-item">