Added galery
This commit is contained in:
82
README.md
82
README.md
@@ -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]
|
||||
```
|
||||
|
||||

|
||||

|
||||

|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
25
_installation/gallery_table.sql
Normal file
25
_installation/gallery_table.sql
Normal 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;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -126,7 +126,7 @@ class DatabaseController extends Controller
|
||||
}
|
||||
|
||||
$structure = DatabaseModel::getDatabaseStructure($database_name);
|
||||
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
@@ -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
|
||||
|
||||
226
application/controller/GalleryController.php
Normal file
226
application/controller/GalleryController.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
323
application/model/GalleryModel.php
Normal file
323
application/model/GalleryModel.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
51
application/view/gallery/edit.php
Normal file
51
application/view/gallery/edit.php
Normal 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>
|
||||
59
application/view/gallery/index.php
Normal file
59
application/view/gallery/index.php
Normal 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>
|
||||
76
application/view/gallery/my.php
Normal file
76
application/view/gallery/my.php
Normal 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>
|
||||
168
application/view/gallery/success.php
Normal file
168
application/view/gallery/success.php
Normal 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>
|
||||
437
application/view/gallery/upload.php
Normal file
437
application/view/gallery/upload.php
Normal 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">×</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>
|
||||
80
application/view/gallery/view.php
Normal file
80
application/view/gallery/view.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
<?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-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>
|
||||
<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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// 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
BIN
assets/screenshots/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/screenshots/img2.png
Normal file
BIN
assets/screenshots/img2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/screenshots/img3.png
Normal file
BIN
assets/screenshots/img3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -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
474
public/css/gallery.css
Normal 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;
|
||||
}
|
||||
12
public/gallery_uploads/.htaccess
Normal file
12
public/gallery_uploads/.htaccess
Normal 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>
|
||||
1
public/gallery_uploads/enc_69773206330d4_1769419270.bin
Normal file
1
public/gallery_uploads/enc_69773206330d4_1769419270.bin
Normal file
File diff suppressed because one or more lines are too long
1
public/gallery_uploads/enc_69773211625f5_1769419281.bin
Normal file
1
public/gallery_uploads/enc_69773211625f5_1769419281.bin
Normal file
File diff suppressed because one or more lines are too long
1
public/gallery_uploads/enc_697733fc9d48c_1769419772.bin
Normal file
1
public/gallery_uploads/enc_697733fc9d48c_1769419772.bin
Normal file
File diff suppressed because one or more lines are too long
1
public/gallery_uploads/enc_69773584ad61d_1769420164.bin
Normal file
1
public/gallery_uploads/enc_69773584ad61d_1769420164.bin
Normal file
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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user