Nano Media AI

This commit is contained in:
Flatlogic Bot 2026-01-26 01:57:49 +00:00
parent c2f7887af4
commit 28596db6d9
5 changed files with 989 additions and 725 deletions

View File

@ -11,46 +11,25 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$type = $_POST['type'] ?? 'photo';
$prompt = $_POST['prompt'] ?? '';
$style = $_POST['style'] ?? '';
$aspect_ratio = $_POST['aspect_ratio'] ?? '1:1';
$rapidapi_key = $_POST['rapidapi_key'] ?? '';
$video_provider = $_POST['video_provider'] ?? 'pexels'; // 'pexels' or 'ai'
if (empty($prompt)) {
echo json_encode(['success' => false, 'error' => 'Запрос не может быть пустым']);
exit;
}
// Map aspect ratios to dimensions
$dimensions = [
'1:1' => [1024, 1024],
'16:9' => [1280, 720],
'9:16' => [720, 1280],
'4:3' => [1024, 768],
'3:2' => [1080, 720]
];
$width = 1024;
$height = 1024;
$width = $dimensions[$aspect_ratio][0] ?? 1024;
$height = $dimensions[$aspect_ratio][1] ?? 1024;
// Enhance prompt with style
$enhanced_prompt = $prompt;
if (!empty($style)) {
$style_map = [
'anime' => 'in anime style, high quality, vibrant colors',
'realism' => 'photorealistic, highly detailed, 8k, masterpiece',
'cyberpunk' => 'cyberpunk style, neon lights, futuristic, highly detailed',
'vaporwave' => 'vaporwave aesthetic, pink and blue colors, retro-futuristic',
'pixel-art' => 'pixel art style, 8-bit, retro game aesthetic',
'fantasy' => 'fantasy art, magical atmosphere, epic, highly detailed',
'3d-render' => '3d render, octane render, cinematic lighting, unreal engine 5',
'steampunk' => 'steampunk style, gears, brass, Victorian aesthetic',
'oil-painting' => 'oil painting, brush strokes, artistic, classic masterpiece',
'sketch' => 'pencil sketch, hand drawn, artistic, charcoal',
'pop-art' => 'pop art style, Andy Warhol aesthetic, vibrant, bold colors',
'3d-render' => '3d render, soft aesthetic, cinematic lighting, high quality',
'minimalism' => 'minimalist style, clean lines, simple, elegant',
'cinematic' => 'cinematic shot, dramatic lighting, movie still, high contrast'
'cinematic' => 'cinematic shot, dramatic lighting, movie still'
];
if (isset($style_map[$style])) {
$enhanced_prompt .= ", " . $style_map[$style];
}
@ -64,8 +43,7 @@ try {
if ($type === 'photo') {
$is_ai = true;
$provider_name = 'Pollinations AI (Flux)';
// Real AI Generation (Truly free)
$provider_name = 'Pollinations Flux';
$seed = rand(1000, 99999);
$encoded_prompt = urlencode($enhanced_prompt);
$api_url = "https://image.pollinations.ai/prompt/{$encoded_prompt}?width={$width}&height={$height}&seed={$seed}&model=flux&nologo=true";
@ -86,94 +64,38 @@ try {
file_put_contents($target, $image_data);
$result_url = $filename;
} else {
// Fallback to direct URL if saving failed
$result_url = $api_url;
}
} else {
// Video Logic
if ($video_provider === 'ai') {
if (!empty($rapidapi_key)) {
$is_ai = true;
$provider_name = 'RapidAPI AI';
// Attempt real AI Video generation
// Note: Different APIs have different formats, this is a generic attempt
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://text-to-video2.p.rapidapi.com/generate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["prompt" => $enhanced_prompt]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-RapidAPI-Host: text-to-video2.p.rapidapi.com",
"X-RapidAPI-Key: " . $rapidapi_key
],
]);
$response = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if (!$err) {
$res_data = json_decode($response, true);
if (isset($res_data['video_url'])) {
$result_url = $res_data['video_url'];
} else if (isset($res_data['url'])) {
$result_url = $res_data['url'];
}
}
}
if (empty($result_url)) {
// Fallback to Pexels search if no key or API failed
$is_ai = false;
$provider_name = 'Pexels Stock (Fallback)';
if (empty($rapidapi_key)) {
$message = 'Ключ RapidAPI не найден. Используется качественный стоковый поиск вместо ИИ-генерации.';
} else {
$message = 'Ошибка API генерации. Используется стоковый поиск.';
}
$url = 'https://api.pexels.com/videos/search?query=' . urlencode($prompt) . '&per_page=1&page=1';
$data = pexels_get($url);
if ($data && !empty($data['videos'])) {
foreach ($data['videos'][0]['video_files'] as $file) {
if ($file['quality'] === 'hd' || $file['quality'] === 'sd') {
$result_url = $file['link'];
break;
}
}
}
}
} else {
// Standard Pexels Stock
$is_ai = false;
$provider_name = 'Pexels Stock';
$url = 'https://api.pexels.com/videos/search?query=' . urlencode($prompt) . '&per_page=1&page=1';
$data = pexels_get($url);
if ($data && !empty($data['videos'])) {
foreach ($data['videos'][0]['video_files'] as $file) {
if ($file['quality'] === 'hd' || $file['quality'] === 'sd') {
$result_url = $file['link'];
break;
}
// Video always from Pexels
$is_ai = false;
$provider_name = 'Pexels Stock';
$url = 'https://api.pexels.com/videos/search?query=' . urlencode($prompt) . '&per_page=1&page=1';
$data = pexels_get($url);
if ($data && !empty($data['videos'])) {
foreach ($data['videos'][0]['video_files'] as $file) {
if ($file['quality'] === 'hd' || $file['quality'] === 'sd') {
$result_url = $file['link'];
break;
}
}
}
if (empty($result_url)) {
$result_url = 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4';
$message = 'Видео не найдено по вашему запросу. Показываем пример.';
}
}
// Save to history
$history_prompt = $prompt . ($style ? " ($style)" : "");
$stmt = db()->prepare("INSERT INTO media_history (type, prompt, result_url) VALUES (?, ?, ?)");
$stmt->execute([$type, $prompt . ($style ? " ($style)" : ""), $result_url]);
$stmt->execute([$type, $history_prompt, $result_url]);
echo json_encode([
'success' => true,
'type' => $type,
'url' => $result_url,
'prompt' => $prompt,
'prompt' => $history_prompt,
'is_ai' => $is_ai,
'provider' => $provider_name,
'message' => $message

40
api/save.php Normal file
View File

@ -0,0 +1,40 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || empty($data['image'])) {
echo json_encode(['success' => false, 'error' => 'No image data provided']);
exit;
}
try {
$img_data = $data['image'];
$img_data = str_replace('data:image/png;base64,', '', $img_data);
$img_data = str_replace(' ', '+', $img_data);
$decoded_data = base64_decode($img_data);
$filename = 'assets/images/pexels/edited_' . md5(uniqid()) . '.png';
$target = __DIR__ . '/../' . $filename;
if (!is_dir(__DIR__ . '/../assets/images/pexels/')) {
mkdir(__DIR__ . '/../assets/images/pexels/', 0775, true);
}
if (file_put_contents($target, $decoded_data)) {
// Save to database
$stmt = db()->prepare("INSERT INTO media_history (type, prompt, result_url) VALUES (?, ?, ?)");
$stmt->execute(['photo', 'Edited in Nano Editor', $filename]);
echo json_encode([
'success' => true,
'url' => $filename,
'message' => 'Сохранено в галерее!'
]);
} else {
echo json_encode(['success' => false, 'error' => 'Failed to save image file']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -1,120 +1,240 @@
/* Custom Styles for AI Media Forge */
/* Nano Banana Aesthetic CSS - Pro Edition */
:root {
--primary-color: #000000;
--accent-color: #007bff;
--bg-light: #f8f9fa;
--card-radius: 16px;
--nano-yellow: #FFDE59;
--nano-yellow-soft: #FFF9E0;
--nano-black: #1A1A1A;
--nano-white: #FFFFFF;
--nano-gray: #F5F5F5;
--nano-radius: 24px;
}
body {
font-family: 'Inter', sans-serif;
color: #333;
background-color: var(--nano-gray);
color: var(--nano-black);
}
.fw-black { font-weight: 900; }
.bg-yellow-soft { background-color: var(--nano-yellow-soft); }
/* Navbar */
.navbar {
backdrop-filter: blur(10px);
background-color: rgba(33, 37, 41, 0.95) !important;
border-bottom: 3px solid var(--nano-black);
}
/* Cards */
.card {
border: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
.card-nano {
background: var(--nano-white);
border: 3px solid var(--nano-black);
border-radius: var(--nano-radius);
box-shadow: 8px 8px 0px var(--nano-black);
transition: all 0.2s ease;
overflow: hidden;
}
.history-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important;
transform: translate(-2px, -2px);
box-shadow: 12px 12px 0px var(--nano-black);
}
/* Buttons */
.btn-primary {
background-color: var(--primary-color);
padding: 10px 24px;
transition: all 0.3s ease;
.btn-nano {
background-color: var(--nano-yellow);
color: var(--nano-black);
border: 3px solid var(--nano-black);
border-radius: var(--nano-radius);
font-weight: 800;
padding: 12px 24px;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-primary:hover {
background-color: #333;
transform: translateY(-1px);
.btn-nano:hover {
background-color: var(--nano-black);
color: var(--nano-yellow);
transform: scale(1.05);
}
/* Editor */
.btn-nano:active {
transform: scale(0.95);
}
/* Form Controls */
.form-control-nano {
border: 3px solid var(--nano-black);
border-radius: 16px;
padding: 12px 16px;
font-weight: 600;
}
.form-control-nano:focus {
box-shadow: none;
background-color: #FFFBE6;
border-color: var(--nano-black);
}
/* Editor Specifics */
.editor-preview-container {
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
border: 3px solid var(--nano-black);
border-radius: 20px;
background-image: radial-gradient(#ccc 1px, transparent 1px);
background-size: 20px 20px;
background-color: #f0f0f0;
box-shadow: inset 0 0 20px rgba(0,0,0,0.05);
}
#editor-canvas {
max-width: 100%;
max-height: 70vh;
}
.canvas-container {
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
border-radius: 4px;
overflow: hidden;
}
/* Tabs */
.nav-tabs {
border-bottom: 2px solid var(--nano-black) !important;
}
.nav-tabs .nav-link {
border: none;
border-radius: 12px 12px 0 0;
margin-right: 2px;
padding: 8px 15px;
font-size: 0.8rem;
background: transparent;
transition: all 0.2s;
}
.nav-tabs .nav-link.active {
background-color: var(--nano-yellow) !important;
border: 2px solid var(--nano-black) !important;
border-bottom: none !important;
}
/* Range Input */
.filter-range {
height: 10px;
-webkit-appearance: none;
background: transparent;
}
.filter-range::-webkit-slider-runnable-track {
background: #ddd;
height: 4px;
border-radius: 2px;
width: 100%;
height: 8px;
cursor: pointer;
background: #E0E0E0;
border-radius: 4px;
border: 2px solid var(--nano-black);
}
.filter-range::-webkit-slider-thumb {
background: var(--primary-color);
margin-top: -6px;
height: 24px;
width: 24px;
border-radius: 50%;
background: var(--nano-yellow);
cursor: pointer;
-webkit-appearance: none;
margin-top: -10px;
border: 3px solid var(--nano-black);
box-shadow: 2px 2px 0px var(--nano-black);
}
/* AI Magic Box */
.ai-magic-box {
border: 1px solid #e0e0e0;
transition: border-color 0.3s ease;
/* Custom Color Picker */
.form-control-color {
width: 50px;
height: 45px;
padding: 5px;
border-radius: 12px;
}
.ai-magic-box:focus-within {
border-color: var(--accent-color);
/* Cropper Overrides */
.cropper-view-box,
.cropper-face {
border-radius: 0;
}
.cropper-line, .cropper-point {
background-color: var(--nano-yellow);
}
/* Badges */
.badge-ai {
font-weight: 600;
letter-spacing: 0.5px;
padding: 6px 12px;
}
/* Transitions */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Responsiveness */
@media (max-width: 768px) {
.display-5 {
font-size: 2rem;
}
}
/* Nano Editor Specifics */
.letter-spacing-1 {
letter-spacing: 1px;
}
.nav-pills .nav-link {
border: 1px solid transparent;
}
.nav-pills .nav-link:not(.active):hover {
background-color: #f0f0f0;
}
#editor-loading {
.badge-nano {
background: var(--nano-yellow);
color: var(--nano-black);
border: 2px solid var(--nano-black);
border-radius: 8px;
padding: 4px 10px;
font-weight: 800;
font-size: 0.75rem;
text-transform: uppercase;
}
textarea#ai-edit-prompt {
resize: none;
border-radius: 8px;
font-size: 0.9rem;
/* Tool Buttons */
.tool-btn {
border: 2px solid var(--nano-black);
border-radius: 12px;
font-weight: 700;
transition: all 0.2s;
}
.btn-outline-dark {
border-radius: 8px;
font-weight: 500;
.tool-btn.active {
background-color: var(--nano-yellow);
box-shadow: 3px 3px 0px var(--nano-black);
transform: translate(-1px, -1px);
}
/* Sticker Items */
.sticker-item {
font-size: 1.5rem;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border: 2px solid #ddd;
border-radius: 10px;
transition: all 0.2s;
}
.sticker-item:hover {
background: var(--nano-yellow-soft);
border-color: var(--nano-yellow);
transform: scale(1.1) rotate(5deg);
}
/* Animations */
@keyframes banana-float {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-10px) rotate(10deg); }
100% { transform: translateY(0) rotate(0deg); }
}
.fas.fa-banana {
animation: banana-float 2s ease-in-out infinite;
display: inline-block;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: var(--nano-black);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #333;
}

View File

@ -1,7 +1,6 @@
document.addEventListener('DOMContentLoaded', () => {
// --- Core UI Elements ---
const form = document.getElementById('generation-form');
const mediaType = document.getElementById('media-type');
const videoProviderContainer = document.getElementById('video-provider-container');
const generateBtn = document.getElementById('generate-btn');
const placeholderText = document.getElementById('placeholder-text');
const loadingState = document.getElementById('loading-state');
@ -12,53 +11,70 @@ document.addEventListener('DOMContentLoaded', () => {
const providerBadge = document.getElementById('provider-badge');
const infoOverlay = document.getElementById('info-overlay');
const statusMessage = document.getElementById('status-message');
const uploadInput = document.getElementById('upload-image');
// Editor Elements
const editorModal = new bootstrap.Modal(document.getElementById('editorModal'));
const editorCanvas = document.getElementById('editor-canvas');
const ctx = editorCanvas.getContext('2d');
const filterRanges = document.querySelectorAll('.filter-range');
const resetFiltersBtn = document.getElementById('reset-filters');
const saveEditedBtn = document.getElementById('save-edited-btn');
// --- Editor Elements ---
const editorModalEl = document.getElementById('editorModal');
const editorModal = new bootstrap.Modal(editorModalEl);
const editorLoading = document.getElementById('editor-loading');
const cropperImg = document.getElementById('cropper-image');
const fabricWrapper = document.getElementById('fabric-wrapper');
const transformBar = document.getElementById('transform-bar');
// Controls
const filterRanges = document.querySelectorAll('.filter-range');
const brushToggle = document.getElementById('brush-toggle');
const brushColor = document.getElementById('brush-color');
const brushSize = document.getElementById('brush-size');
const textInput = document.getElementById('text-input');
const addTextBtn = document.getElementById('add-text-btn');
const textColor = document.getElementById('text-color');
const fontFamily = document.getElementById('font-family');
const stickerItems = document.querySelectorAll('.sticker-item');
const startCropBtn = document.getElementById('start-crop-btn');
const applyCropBtn = document.getElementById('apply-crop-btn');
const resetEditorBtn = document.getElementById('reset-editor');
const saveEditedBtn = document.getElementById('save-edited-btn');
const saveToGalleryBtn = document.getElementById('save-to-gallery-btn');
// AI Magic Elements
const aiEditPrompt = document.getElementById('ai-edit-prompt');
const applyAiMagicBtn = document.getElementById('apply-ai-magic');
const removeBgBtn = document.getElementById('remove-bg-btn');
const upscaleBtn = document.getElementById('upscale-btn');
let currentImage = null;
// --- Editor State ---
let canvas = null;
let cropper = null;
let fabricImage = null; // background image
let currentResultUrl = '';
let originalPrompt = '';
let isDrawing = false;
// Local storage for settings
const getRapidKey = () => localStorage.getItem('rapidapi_key') || '';
const setRapidKey = (key) => localStorage.setItem('rapidapi_key', key);
// --- Generation Logic ---
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
originalPrompt = formData.get('prompt');
// Sync input with local storage
const keyInput = document.getElementById('rapidapi-key-input');
if (keyInput) {
keyInput.value = getRapidKey();
keyInput.addEventListener('change', (e) => setRapidKey(e.target.value));
}
showLoading();
// Toggle video provider field
mediaType.addEventListener('change', () => {
if (mediaType.value === 'video') {
videoProviderContainer.style.display = 'block';
} else {
videoProviderContainer.style.display = 'none';
try {
const response = await fetch('api/generate.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) renderResult(result);
else alert('Ошибка: ' + (result.error || 'Сбой генерации'));
} catch (error) {
alert('Сетевая ошибка');
} finally {
hideLoading();
}
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
formData.append('rapidapi_key', getRapidKey());
originalPrompt = formData.get('prompt');
// UI State: Loading
function showLoading() {
placeholderText.classList.add('d-none');
contentContainer.classList.add('d-none');
actionButtons.classList.add('d-none');
@ -66,155 +82,303 @@ document.addEventListener('DOMContentLoaded', () => {
statusMessage.classList.add('d-none');
loadingState.classList.remove('d-none');
generateBtn.disabled = true;
}
try {
const response = await fetch('api/generate.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
renderResult(result);
} else {
alert('Ошибка: ' + (result.error || 'Что-то пошло не так'));
resetUI();
}
} catch (error) {
console.error('Generation error:', error);
alert('Сетевая ошибка при генерации');
resetUI();
} finally {
loadingState.classList.add('d-none');
generateBtn.disabled = false;
}
});
function hideLoading() {
loadingState.classList.add('d-none');
generateBtn.disabled = false;
}
function renderResult(result) {
contentContainer.innerHTML = '';
contentContainer.classList.remove('d-none');
actionButtons.classList.remove('d-none');
infoOverlay.classList.remove('d-none');
currentResultUrl = result.url;
providerBadge.textContent = result.provider;
if (result.is_ai) {
providerBadge.className = 'badge badge-ai shadow-sm';
} else {
providerBadge.className = 'badge badge-stock shadow-sm';
}
if (result.message) {
statusMessage.textContent = result.message;
statusMessage.classList.remove('d-none');
}
providerBadge.className = result.is_ai ? 'badge-nano bg-info' : 'badge-nano bg-warning';
if (result.type === 'photo') {
const img = document.createElement('img');
img.src = result.url;
img.className = 'img-fluid shadow-sm rounded mx-auto d-block';
img.style.maxHeight = '480px';
img.style.objectFit = 'contain';
img.id = 'active-result-img';
contentContainer.appendChild(img);
editBtn.classList.remove('d-none');
editBtn.onclick = () => {
originalPrompt = result.prompt || originalPrompt;
openEditor(result.url);
};
} else {
const video = document.createElement('video');
video.src = result.url;
video.controls = true;
video.autoplay = true;
video.className = 'rounded mx-auto d-block shadow-sm';
video.style.maxWidth = '100%';
video.style.maxHeight = '480px';
contentContainer.appendChild(video);
editBtn.classList.add('d-none');
}
}
downloadBtn.onclick = () => {
const a = document.createElement('a');
a.href = result.url;
a.download = `generation_${Date.now()}.${result.type === 'photo' ? 'jpg' : 'mp4'}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// --- Upload Handler ---
uploadInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (f) => {
originalPrompt = "Uploaded image";
openEditor(f.target.result);
};
reader.readAsDataURL(file);
});
// --- Editor Initialization ---
function initFabric() {
if (canvas) return;
canvas = new fabric.Canvas('editor-canvas', {
isDrawingMode: false,
preserveObjectStacking: true
});
// Handle object deletion
window.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
deleteObject();
}
});
}
function resetUI() {
placeholderText.classList.remove('d-none');
contentContainer.classList.add('d-none');
actionButtons.classList.add('d-none');
infoOverlay.classList.add('d-none');
}
window.deleteObject = () => {
const activeObjects = canvas.getActiveObjects();
if (activeObjects.length && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) {
canvas.remove(...activeObjects);
canvas.discardActiveObject();
canvas.requestRenderAll();
}
};
window.bringToFront = () => {
const active = canvas.getActiveObject();
if (active) {
active.bringToFront();
canvas.requestRenderAll();
}
};
window.sendToBack = () => {
const active = canvas.getActiveObject();
if (active) {
// Keep background image at the very bottom
active.sendToBack();
if (fabricImage) fabricImage.sendToBack();
canvas.requestRenderAll();
}
};
// Editor Logic
function openEditor(url) {
currentImage = new Image();
currentImage.crossOrigin = "Anonymous";
currentImage.src = url;
currentImage.onload = () => {
editorCanvas.width = currentImage.width;
editorCanvas.height = currentImage.height;
applyFilters();
editorModal.show();
};
}
function applyFilters() {
if (!currentImage) return;
let filters = '';
filterRanges.forEach(range => {
const filter = range.dataset.filter;
const value = range.value;
if (filter === 'brightness' || filter === 'contrast' || filter === 'saturate') {
filters += `${filter}(${value}%) `;
} else if (filter === 'blur') {
filters += `${filter}(${value}px) `;
} else {
filters += `${filter}(${value}%) `;
}
});
ctx.filter = filters;
ctx.clearRect(0, 0, editorCanvas.width, editorCanvas.height);
ctx.drawImage(currentImage, 0, 0);
}
filterRanges.forEach(range => {
range.addEventListener('input', applyFilters);
});
resetFiltersBtn.addEventListener('click', () => {
filterRanges.forEach(range => {
if (range.dataset.filter === 'brightness' || range.dataset.filter === 'contrast' || range.dataset.filter === 'saturate') {
range.value = 100;
} else {
range.value = 0;
}
});
applyFilters();
});
saveEditedBtn.addEventListener('click', () => {
const link = document.createElement('a');
link.download = `edited_${Date.now()}.png`;
link.href = editorCanvas.toDataURL('image/png');
link.click();
});
// AI Magic Handlers
async function performAiEdit(action, customPrompt = '') {
if (!currentImage) return;
initFabric();
editorLoading.classList.remove('d-none');
editorLoading.classList.add('d-flex');
// Reset State
canvas.clear();
isDrawing = false;
brushToggle.classList.remove('active');
canvas.isDrawingMode = false;
fabric.Image.fromURL(url, (img) => {
fabricImage = img;
const maxDimension = 1000;
if (img.width > maxDimension || img.height > maxDimension) {
const scale = maxDimension / Math.max(img.width, img.height);
img.scale(scale);
}
canvas.setWidth(img.getScaledWidth());
canvas.setHeight(img.getScaledHeight());
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
// Sync ranges
filterRanges.forEach(r => {
const f = r.dataset.filter;
r.value = (f === 'brightness' || f === 'contrast' || f === 'saturate') ? 100 : 0;
const vDisplay = document.getElementById(`val-${f}`);
if (vDisplay) vDisplay.textContent = r.value;
});
editorLoading.classList.add('d-none');
editorLoading.classList.remove('d-flex');
editorModal.show();
}, { crossOrigin: 'anonymous' });
}
editBtn.addEventListener('click', () => currentResultUrl && openEditor(currentResultUrl));
downloadBtn.addEventListener('click', () => {
if (!currentResultUrl) return;
const a = document.createElement('a');
a.href = currentResultUrl;
a.download = `nano_${Date.now()}.${currentResultUrl.includes('.mp4') ? 'mp4' : 'jpg'}`;
a.click();
});
// --- Filter Logic ---
filterRanges.forEach(range => {
range.addEventListener('input', () => {
const filterType = range.dataset.filter;
const val = parseFloat(range.value);
const vDisplay = document.getElementById(`val-${filterType}`);
if (vDisplay) vDisplay.textContent = val;
applyFabricFilters();
});
});
function applyFabricFilters() {
if (!fabricImage) return;
const f = fabric.Image.filters;
fabricImage.filters = [];
filterRanges.forEach(r => {
const type = r.dataset.filter;
const v = parseFloat(r.value);
if (type === 'brightness' && v !== 100) fabricImage.filters.push(new f.Brightness({ brightness: (v - 100) / 100 }));
if (type === 'contrast' && v !== 100) fabricImage.filters.push(new f.Contrast({ contrast: (v - 100) / 100 }));
if (type === 'saturate' && v !== 100) fabricImage.filters.push(new f.Saturation({ saturation: (v - 100) / 100 }));
if (type === 'blur' && v > 0) fabricImage.filters.push(new f.Blur({ blur: v / 20 }));
if (type === 'sepia' && v > 0) fabricImage.filters.push(new f.Sepia());
if (type === 'grayscale' && v > 0) fabricImage.filters.push(new f.Grayscale());
if (type === 'hue-rotate' && v > 0) fabricImage.filters.push(new f.HueRotation({ rotation: v }));
if (type === 'noise' && v > 0) fabricImage.filters.push(new f.Noise({ noise: v * 2 }));
if (type === 'vignette' && v > 0) fabricImage.filters.push(new f.Vignette({ brightness: v / 100 }));
});
fabricImage.applyFilters();
canvas.requestRenderAll();
}
// --- Transform Logic ---
window.rotateLeft = () => rotateCanvas(-90);
window.rotateRight = () => rotateCanvas(90);
window.flipH = () => {
if (!fabricImage) return;
fabricImage.set('flipX', !fabricImage.flipX);
canvas.requestRenderAll();
};
window.flipV = () => {
if (!fabricImage) return;
fabricImage.set('flipY', !fabricImage.flipY);
canvas.requestRenderAll();
};
function rotateCanvas(degrees) {
if (!fabricImage) return;
const angle = (fabricImage.angle + degrees) % 360;
fabricImage.set('angle', angle);
if (Math.abs(degrees) % 180 !== 0) {
const w = canvas.width;
const h = canvas.height;
canvas.setDimensions({ width: h, height: w });
}
canvas.centerObject(fabricImage);
fabricImage.setCoords();
canvas.requestRenderAll();
}
// --- Cropper Logic ---
startCropBtn.addEventListener('click', () => {
const dataUrl = canvas.toDataURL({ format: 'png' });
fabricWrapper.style.display = 'none';
cropperImg.src = dataUrl;
cropperImg.style.display = 'block';
transformBar.classList.remove('d-none');
if (cropper) cropper.destroy();
cropper = new Cropper(cropperImg, {
viewMode: 1,
autoCropArea: 0.8,
responsive: true
});
});
applyCropBtn.addEventListener('click', () => {
if (!cropper) return;
const croppedCanvas = cropper.getCroppedCanvas();
const croppedDataUrl = croppedCanvas.toDataURL();
cropper.destroy();
cropper = null;
cropperImg.style.display = 'none';
fabricWrapper.style.display = 'block';
transformBar.classList.add('d-none');
fabric.Image.fromURL(croppedDataUrl, (img) => {
fabricImage = img;
canvas.clear();
canvas.setWidth(img.width);
canvas.setHeight(img.height);
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
});
});
// --- Decor Logic ---
brushToggle.addEventListener('click', () => {
isDrawing = !isDrawing;
canvas.isDrawingMode = isDrawing;
brushToggle.classList.toggle('active', isDrawing);
canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);
canvas.freeDrawingBrush.color = brushColor.value;
canvas.freeDrawingBrush.width = parseInt(brushSize.value, 10);
});
brushColor.addEventListener('input', () => {
if (canvas.freeDrawingBrush) canvas.freeDrawingBrush.color = brushColor.value;
});
brushSize.addEventListener('input', () => {
if (canvas.freeDrawingBrush) canvas.freeDrawingBrush.width = parseInt(brushSize.value, 10);
});
addTextBtn.addEventListener('click', () => {
const textStr = textInput.value.trim() || 'Nano!';
const text = new fabric.IText(textStr, {
left: canvas.width / 2,
top: canvas.height / 2,
fontFamily: fontFamily.value,
fill: textColor.value,
fontSize: 50,
originX: 'center',
originY: 'center',
cornerStyle: 'circle',
cornerColor: '#FFDE59',
cornerStrokeColor: '#000',
transparentCorners: false
});
canvas.add(text);
canvas.setActiveObject(text);
textInput.value = '';
});
stickerItems.forEach(item => {
item.addEventListener('click', () => {
const char = item.dataset.sticker;
const sticker = new fabric.Text(char, {
fontSize: 100,
left: canvas.width / 2,
top: canvas.height / 2,
originX: 'center',
originY: 'center',
cornerStyle: 'circle'
});
canvas.add(sticker);
canvas.setActiveObject(sticker);
});
});
// --- AI Magic ---
async function performAiEdit(action, customPrompt = '') {
editorLoading.classList.remove('d-none');
editorLoading.classList.add('d-flex');
const currentDataUrl = canvas.toDataURL({ format: 'png' });
try {
const response = await fetch('api/edit.php', {
@ -224,62 +388,87 @@ document.addEventListener('DOMContentLoaded', () => {
action: action,
original_prompt: originalPrompt,
edit_prompt: customPrompt,
image_url: currentImage.src
image_url: currentDataUrl
})
});
const result = await response.json();
if (result.success) {
// Load new image into canvas
const newImg = new Image();
newImg.crossOrigin = "Anonymous";
newImg.src = result.url + '?t=' + Date.now();
newImg.onload = () => {
currentImage = newImg;
editorCanvas.width = newImg.width;
editorCanvas.height = newImg.height;
applyFilters();
fabric.Image.fromURL(result.url + '?t=' + Date.now(), (img) => {
fabricImage = img;
canvas.clear();
canvas.setWidth(img.getScaledWidth());
canvas.setHeight(img.getScaledHeight());
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
editorLoading.classList.add('d-none');
editorLoading.classList.remove('d-flex');
};
}, { crossOrigin: 'anonymous' });
} else {
alert('Ошибка ИИ: ' + (result.error || 'Не удалось применить изменения'));
alert('Ошибка ИИ: ' + result.error);
editorLoading.classList.add('d-none');
editorLoading.classList.remove('d-flex');
}
} catch (error) {
console.error('AI Edit error:', error);
alert('Сетевая ошибка при работе с ИИ');
} catch (e) {
alert('Ошибка связи с сервером');
editorLoading.classList.add('d-none');
editorLoading.classList.remove('d-flex');
}
}
applyAiMagicBtn.addEventListener('click', () => {
const prompt = aiEditPrompt.value.trim();
if (!prompt) {
alert('Введите описание изменений');
return;
const p = aiEditPrompt.value.trim();
if (!p) return alert('Опишите изменения');
performAiEdit('magic', p);
});
removeBgBtn.addEventListener('click', () => performAiEdit('remove_bg'));
upscaleBtn.addEventListener('click', () => performAiEdit('upscale'));
// --- Finalize & Save ---
resetEditorBtn.addEventListener('click', () => {
if (confirm('Сбросить все изменения?')) {
openEditor(fabricImage._originalElement.src);
}
performAiEdit('magic', prompt);
});
removeBgBtn.addEventListener('click', () => {
performAiEdit('remove_bg');
saveEditedBtn.addEventListener('click', () => {
const dataUrl = canvas.toDataURL({ format: 'png', quality: 1.0 });
const link = document.createElement('a');
link.download = `nano_edit_${Date.now()}.png`;
link.href = dataUrl;
link.click();
});
upscaleBtn.addEventListener('click', () => {
performAiEdit('upscale');
saveToGalleryBtn.addEventListener('click', async () => {
saveToGalleryBtn.disabled = true;
saveToGalleryBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> СОХРАНЯЕМ...';
const dataUrl = canvas.toDataURL({ format: 'png', quality: 1.0 });
try {
const response = await fetch('api/save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: dataUrl })
});
const result = await response.json();
if (result.success) {
alert(result.message);
location.reload(); // Refresh to see in gallery
} else {
alert('Ошибка: ' + result.error);
}
} catch (e) {
alert('Ошибка сети');
} finally {
saveToGalleryBtn.disabled = false;
saveToGalleryBtn.innerHTML = '<i class="fas fa-cloud-upload-alt me-2"></i> СОХРАНИТЬ В ГАЛЕРЕЮ';
}
});
// History Edit Buttons
// History interaction
document.addEventListener('click', (e) => {
if (e.target.closest('.history-edit-btn')) {
const btn = e.target.closest('.history-edit-btn');
const url = btn.dataset.url;
const card = btn.closest('.card');
originalPrompt = card.querySelector('.card-text').textContent;
const hBtn = e.target.closest('.history-edit-btn');
if (hBtn) {
const url = hBtn.dataset.url;
const prompt = hBtn.closest('.history-card').querySelector('.history-prompt').textContent;
originalPrompt = prompt;
openEditor(url);
}
});

727
index.php
View File

@ -1,7 +1,7 @@
<?php
require_once 'db/config.php';
$project_name = $_SERVER['PROJECT_NAME'] ?? 'AI Media Forge';
$project_description = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Generate unique AI photos and videos for free.';
$project_name = $_SERVER['PROJECT_NAME'] ?? 'Nano Media AI';
$project_description = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Generate unique AI photos and high-quality stock videos.';
?>
<!DOCTYPE html>
<html lang="ru">
@ -13,381 +13,436 @@ $project_description = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Generate unique AI ph
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Google Fonts: Inter -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Google Fonts: Inter & Montserrat -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@800&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Editor Libraries -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<style>
.badge-ai { background: linear-gradient(45deg, #007bff, #6610f2); }
.badge-stock { background: #6c757d; }
.form-select, .form-control { border-radius: 8px; }
.btn-primary { border-radius: 8px; background: #000; border: none; }
.btn-primary:hover { background: #333; }
.card { border-radius: 16px; }
.editor-preview-container {
background: #1a1a1a;
border-radius: 8px;
:root {
--nano-yellow: #FFDE59;
--nano-black: #1A1A1A;
--nano-white: #FFFFFF;
--nano-gray: #F5F5F5;
--nano-radius: 24px;
}
body {
background-color: var(--nano-gray);
font-family: 'Inter', sans-serif;
}
.navbar {
background-color: var(--nano-white) !important;
border-bottom: 2px solid var(--nano-black);
padding: 15px 0;
}
.navbar-brand {
font-family: 'Montserrat', sans-serif;
color: var(--nano-black) !important;
font-size: 1.5rem;
text-transform: uppercase;
}
.btn-nano {
background-color: var(--nano-yellow);
color: var(--nano-black);
border: 2px solid var(--nano-black);
border-radius: var(--nano-radius);
font-weight: 700;
padding: 10px 25px;
transition: all 0.2s ease;
}
.btn-nano:hover {
background-color: var(--nano-black);
color: var(--nano-yellow);
transform: translateY(-2px);
}
.card-nano {
background: var(--nano-white);
border: 2px solid var(--nano-black);
border-radius: var(--nano-radius);
box-shadow: 8px 8px 0px var(--nano-black);
overflow: hidden;
}
.form-control-nano {
border: 2px solid var(--nano-black);
border-radius: 15px;
padding: 12px;
font-weight: 500;
}
.form-control-nano:focus {
box-shadow: none;
border-color: var(--nano-yellow);
background-color: #fffde7;
}
.badge-nano {
background: var(--nano-yellow);
color: var(--nano-black);
border: 1px solid var(--nano-black);
border-radius: 10px;
padding: 5px 12px;
font-weight: 600;
}
.editor-preview-container {
background: #eee;
border: 2px solid var(--nano-black);
border-radius: var(--nano-radius);
overflow: hidden;
position: relative;
}
.canvas-container {
margin: 0 auto;
}
.sticker-item {
cursor: pointer;
transition: transform 0.2s;
border: 2px solid transparent;
border-radius: 10px;
padding: 5px;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
position: relative;
width: 50px;
height: 50px;
}
#editor-canvas {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
.sticker-item:hover {
transform: scale(1.1);
border-color: var(--nano-yellow);
}
.filter-control label {
font-size: 0.8rem;
font-weight: 600;
color: #444;
display: block;
margin-bottom: 5px;
}
.nav-pills .nav-link {
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #666;
border-radius: 8px;
padding: 8px 16px;
}
.nav-pills .nav-link.active {
background-color: #000;
color: #fff;
}
.ai-magic-box {
background: #f8f9fa;
.tool-btn {
border: 2px solid var(--nano-black);
border-radius: 12px;
padding: 15px;
border: 1px solid #eee;
padding: 8px;
background: white;
transition: all 0.2s;
}
#editor-loading {
background: rgba(0,0,0,0.7);
z-index: 10;
.tool-btn:hover, .tool-btn.active {
background: var(--nano-yellow);
}
</style>
</head>
<body class="bg-light">
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<nav class="navbar navbar-expand-lg sticky-top">
<div class="container">
<a class="navbar-brand fw-bold" href="/">
<i class="fas fa-robot me-2"></i><?php echo htmlspecialchars($project_name); ?>
<i class="fas fa-banana text-warning me-2"></i><?php echo htmlspecialchars($project_name); ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
<li class="nav-item"><a class="nav-link active" href="#">Генерация</a></li>
<li class="nav-item"><a class="nav-link" href="#history">История</a></li>
<li class="nav-item">
<button class="btn btn-outline-light btn-sm ms-lg-3" data-bs-toggle="modal" data-bs-target="#settingsModal">
<i class="fas fa-cog"></i> Настройки
</button>
</li>
</ul>
<div class="ms-auto d-flex align-items-center">
<a href="#history" class="btn btn-nano btn-sm">История</a>
</div>
</div>
</nav>
<main class="container py-5">
<!-- Hero & Generator Section -->
<section class="row justify-content-center mb-5">
<div class="col-lg-10 text-center mb-4">
<h1 class="display-5 fw-bold text-dark mb-2">Создавайте ИИ-контент мгновенно</h1>
<p class="lead text-muted">Используйте мощные API для генерации уникальных изображений и видео по текстовому описанию.</p>
</div>
<!-- Hero -->
<div class="text-center mb-5">
<h1 class="display-4 fw-black mb-3" style="font-family: 'Montserrat', sans-serif;">NANO GENERATOR 🍌</h1>
<p class="lead text-dark fw-medium">Создавай крутые ИИ фото и качественные сток-видео.</p>
</div>
<!-- Generator Section -->
<section class="row justify-content-center mb-5">
<div class="col-lg-12">
<div class="card border-0 shadow-lg overflow-hidden">
<div class="card-body p-0">
<div class="bg-white p-4 p-md-4 border-bottom">
<form id="generation-form">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase">Тип контента</label>
<select class="form-select border-2" id="media-type" name="type">
<option value="photo">Изображение (Real AI)</option>
<option value="video">Видео</option>
</select>
</div>
<div class="col-md-3" id="video-provider-container" style="display: none;">
<label class="form-label small fw-bold text-uppercase">Провайдер</label>
<select class="form-select border-2" id="video-provider" name="video_provider">
<option value="pexels">Сток (Всегда Бесплатно)</option>
<option value="ai">ИИ (Требуется Ключ)</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase">Стиль</label>
<select class="form-select border-2" id="style" name="style">
<option value="">Без стиля</option>
<option value="anime">Аниме</option>
<option value="realism">Реализм</option>
<option value="cyberpunk">Киберпанк</option>
<option value="vaporwave">Вейпорвейв</option>
<option value="pixel-art">Пиксель-арт</option>
<option value="fantasy">Фэнтези</option>
<option value="3d-render">3D Рендер</option>
<option value="steampunk">Стимпанк</option>
<option value="oil-painting">Масляная живопись</option>
<option value="sketch">Скетч</option>
<option value="pop-art">Поп-арт</option>
<option value="minimalism">Минимализм</option>
<option value="cinematic">Кинематографичный</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold text-uppercase">Формат</label>
<select class="form-select border-2" id="aspect_ratio" name="aspect_ratio">
<option value="1:1">1:1 Квадрат</option>
<option value="16:9">16:9 Широкий</option>
<option value="9:16">9:16 Вертикаль</option>
<option value="4:3">4:3 Классик</option>
<option value="3:2">3:2 Фото</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase">Ваш запрос</label>
<input type="text" class="form-control border-2" id="prompt" name="prompt" placeholder="Например: 'Космонавт на лошади'..." required>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100 fw-bold py-2" id="generate-btn">
Создать
</button>
</div>
</div>
</form>
<div class="card-nano p-4">
<form id="generation-form">
<div class="row g-3">
<div class="col-md-2">
<label class="form-label small fw-bold">ТИП КОНТЕНТА</label>
<select class="form-select form-control-nano" id="media-type" name="type">
<option value="photo">ФОТО (ИИ)</option>
<option value="video">ВИДЕО (STOCK)</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">СТИЛЬ</label>
<select class="form-select form-control-nano" id="style" name="style">
<option value="">ОРИГИНАЛ</option>
<option value="anime">АНИМЕ</option>
<option value="cyberpunk">КИБЕРПАНК</option>
<option value="3d-render">3D SOFT</option>
<option value="minimalism">МИНИМАЛИЗМ</option>
<option value="cinematic">КИНО</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">ТВОЙ ЗАПРОС</label>
<input type="text" class="form-control form-control-nano" id="prompt" name="prompt" placeholder="Что нарисуем или найдем?.." required>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-nano w-100" id="generate-btn">
СОЗДАТЬ
</button>
</div>
<div class="col-md-2 d-flex align-items-end">
<input type="file" id="upload-image" accept="image/*" class="d-none">
<button type="button" class="btn btn-outline-dark w-100 rounded-pill py-2" onclick="document.getElementById('upload-image').click()">
<i class="fas fa-upload me-1"></i> СВОЁ ФОТО
</button>
</div>
</div>
</form>
<div id="result-preview" class="mt-4 position-relative" style="min-height: 400px; background: #fafafa; border: 2px dashed #ccc; border-radius: 20px; display: flex; align-items: center; justify-content: center;">
<div class="text-center text-muted" id="placeholder-text">
<i class="fas fa-image fa-3x mb-3"></i>
<p class="fw-bold">ТУТ БУДЕТ МАГИЯ</p>
</div>
<div class="loading-spinner text-center d-none" id="loading-state">
<div class="spinner-border text-dark" role="status"></div>
<p class="mt-3 fw-bold">ГОТОВИМ БАНАНЫ...</p>
</div>
<div id="result-preview" class="bg-dark position-relative" style="min-height: 500px; display: flex; align-items: center; justify-content: center;">
<div class="text-center text-secondary" id="placeholder-text">
<i class="fas fa-wand-magic-sparkles fa-3x mb-3 d-block"></i>
<p>Ваше творение появится здесь</p>
</div>
<div class="loading-spinner text-center d-none" id="loading-state">
<div class="spinner-grow text-primary" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light fw-medium">Магия ИИ в процессе...</p>
<small class="text-muted d-block mt-2">Генерация может занять некоторое время</small>
</div>
<div id="content-container" class="d-none w-100 h-100 p-2 text-center"></div>
<div id="content-container" class="d-none w-100 h-100 p-3 text-center">
<!-- Generated content will be injected here -->
</div>
<div id="info-overlay" class="position-absolute top-0 start-0 p-3 d-none">
<span class="badge-nano" id="provider-badge"></span>
</div>
<!-- Info Overlay -->
<div id="info-overlay" class="position-absolute top-0 start-0 p-3 d-none">
<span class="badge badge-ai shadow-sm" id="provider-badge"></span>
</div>
<!-- Action Overlay -->
<div id="action-buttons" class="position-absolute bottom-0 end-0 p-3 d-none d-flex gap-2">
<button class="btn btn-warning btn-sm shadow-sm d-none" id="edit-btn">
<i class="fas fa-magic me-1"></i> Редактировать
</button>
<button class="btn btn-light btn-sm shadow-sm" id="download-btn">
<i class="fas fa-download me-1"></i> Скачать
</button>
</div>
<div id="action-buttons" class="position-absolute bottom-0 end-0 p-3 d-none d-flex gap-2">
<button class="btn btn-nano btn-sm" id="edit-btn">
<i class="fas fa-wand-magic-sparkles"></i> EDIT
</button>
<button class="btn btn-dark btn-sm rounded-pill" id="download-btn">
<i class="fas fa-download"></i>
</button>
</div>
</div>
</div>
<div id="status-message" class="mt-3 alert alert-info d-none" role="alert">
<!-- Status/Warning messages go here -->
</div>
<div class="mt-4 text-center">
<p class="small text-muted">
<i class="fas fa-info-circle me-1"></i>
Генерация <strong>фото</strong> работает без ключа. Для <strong>видео через ИИ</strong> нужен бесплатный ключ <a href="#" data-bs-toggle="modal" data-bs-target="#settingsModal">RapidAPI</a>.
</p>
</div>
<div id="status-message" class="mt-3 alert alert-warning border-2 border-dark d-none" role="alert"></div>
</div>
</section>
<!-- History & Gallery Section -->
<!-- History -->
<section id="history" class="py-5">
<div class="d-flex align-items-center justify-content-between mb-4">
<h2 class="h3 fw-bold mb-0">Недавние генерации</h2>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-black mb-0" style="font-family: 'Montserrat', sans-serif;">ГАЛЕРЕЯ 📂</h2>
<button class="btn btn-outline-dark btn-sm rounded-pill" onclick="location.reload()"><i class="fas fa-sync"></i> Обновить</button>
</div>
<div class="row g-4" id="history-grid">
<?php
try {
$stmt = db()->query("SELECT * FROM media_history ORDER BY created_at DESC LIMIT 6");
$stmt = db()->query("SELECT * FROM media_history ORDER BY created_at DESC LIMIT 12");
$history = $stmt->fetchAll();
if ($history):
foreach ($history as $item):
foreach ($history as $item):
?>
<div class="col-md-4 col-sm-6">
<div class="card h-100 border-0 shadow-sm history-card">
<div class="position-relative">
<?php if ($item['type'] === 'photo'): ?>
<img src="<?php echo htmlspecialchars($item['result_url']); ?>" class="card-img-top history-img" data-url="<?php echo htmlspecialchars($item['result_url']); ?>" alt="AI result" style="height: 250px; object-fit: contain; background: #222;">
<?php else: ?>
<div class="bg-dark d-flex align-items-center justify-content-center" style="height: 250px;">
<video class="w-100 h-100" style="object-fit: contain;" muted onmouseover="this.play()" onmouseout="this.pause();this.currentTime=0;">
<source src="<?php echo htmlspecialchars($item['result_url']); ?>" type="video/mp4">
</video>
<i class="fas fa-play position-absolute text-white opacity-50"></i>
<div class="card-nano h-100 history-card">
<?php if ($item['type'] === 'photo'): ?>
<img src="<?php echo htmlspecialchars($item['result_url']); ?>" class="w-100" style="height: 250px; object-fit: cover; border-bottom: 2px solid #000;">
<?php else: ?>
<div class="bg-dark" style="height: 250px; border-bottom: 2px solid #000; position: relative;">
<video class="w-100 h-100" style="object-fit: cover;" muted onmouseover="this.play()" onmouseout="this.pause()">
<source src="<?php echo htmlspecialchars($item['result_url']); ?>" type="video/mp4">
</video>
<div class="position-absolute top-50 start-50 translate-middle pointer-events-none">
<i class="fas fa-play text-white fa-2x opacity-50"></i>
</div>
<?php endif; ?>
<span class="badge bg-dark position-absolute top-0 end-0 m-2 opacity-75">
<?php echo $item['type'] === 'photo' ? 'PHOTO' : 'VIDEO'; ?>
</span>
</div>
<div class="card-body">
<p class="card-text small text-truncate mb-2 fw-medium"><?php echo htmlspecialchars($item['prompt']); ?></p>
</div>
<?php endif; ?>
<div class="p-3">
<p class="small fw-bold text-truncate mb-2 history-prompt"><?php echo htmlspecialchars($item['prompt']); ?></p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted"><?php echo date('d.m, H:i', strtotime($item['created_at'])); ?></small>
<div>
<span class="badge-nano py-1 px-2" style="font-size: 0.7rem;"><?php echo strtoupper($item['type']); ?></span>
<div class="d-flex gap-2">
<?php if ($item['type'] === 'photo'): ?>
<button class="btn btn-outline-warning btn-sm border-0 history-edit-btn" data-url="<?php echo htmlspecialchars($item['result_url']); ?>">
<button class="btn btn-sm btn-outline-dark rounded-pill history-edit-btn" data-url="<?php echo htmlspecialchars($item['result_url']); ?>">
<i class="fas fa-magic"></i>
</button>
<?php endif; ?>
<a href="<?php echo htmlspecialchars($item['result_url']); ?>" target="_blank" class="btn btn-link btn-sm p-0 text-decoration-none ms-1">Открыть <i class="fas fa-external-link-alt ms-1 small"></i></a>
<a href="<?php echo htmlspecialchars($item['result_url']); ?>" download class="btn btn-sm btn-dark rounded-pill">
<i class="fas fa-download"></i>
</a>
</div>
</div>
</div>
</div>
</div>
<?php
endforeach;
else:
echo '<div class="col-12 text-center py-5 text-muted"><p>История пуста. Создайте что-нибудь!</p></div>';
endif;
} catch (Exception $e) {
echo '<div class="col-12 text-center py-5 text-danger"><p>Ошибка загрузки истории.</p></div>';
}
?>
<?php endforeach; } catch (Exception $e) {} ?>
</div>
</section>
</main>
<!-- Photo Editor Modal -->
<div class="modal fade" id="editorModal" tabindex="-1" aria-hidden="true">
<!-- Nano Editor Modal -->
<div class="modal fade" id="editorModal" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-dark text-white border-0">
<h5 class="modal-title fw-bold"><i class="fas fa-magic me-2"></i> Nano Editor AI</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
<div class="modal-content card-nano border-0">
<div class="modal-header border-bottom border-2 border-dark bg-yellow-soft">
<h5 class="modal-title fw-black"><i class="fas fa-banana text-warning"></i> NANO EDITOR AI PRO</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div class="row g-0">
<!-- Preview Area -->
<div class="col-lg-8">
<div class="editor-preview-container p-3 h-100">
<canvas id="editor-canvas"></canvas>
<!-- Loading Overlay for AI edits -->
<div id="editor-loading" class="position-absolute top-0 start-0 w-100 h-100 d-none flex-column align-items-center justify-content-center text-white">
<div class="spinner-border text-primary mb-3" role="status"></div>
<span class="fw-bold">Применяем ИИ магию...</span>
<div class="col-lg-8 bg-light p-3 border-end border-2 border-dark d-flex flex-column">
<div class="editor-preview-container d-flex align-items-center justify-content-center flex-grow-1" style="min-height: 500px;">
<div id="fabric-wrapper">
<canvas id="editor-canvas"></canvas>
</div>
<img id="cropper-image" src="" style="display: none; max-width: 100%;">
<div id="editor-loading" class="position-absolute top-0 start-0 w-100 h-100 d-none flex-column align-items-center justify-content-center" style="background: rgba(255,255,255,0.8); z-index: 10;">
<div class="spinner-border text-dark"></div>
<span class="fw-bold mt-2">МАГИЯ В ПРОЦЕССЕ...</span>
</div>
</div>
<!-- Transform Controls (Quick Access) -->
<div id="transform-bar" class="mt-3 p-2 bg-white rounded-3 border border-2 border-dark d-flex justify-content-center gap-3 d-none">
<button class="btn btn-sm tool-btn" onclick="rotateLeft()"><i class="fas fa-undo"></i> -90°</button>
<button class="btn btn-sm tool-btn" onclick="rotateRight()"><i class="fas fa-redo"></i> +90°</button>
<button class="btn btn-sm tool-btn" onclick="flipH()"><i class="fas fa-arrows-alt-h"></i> Flip H</button>
<button class="btn btn-sm tool-btn" onclick="flipV()"><i class="fas fa-arrows-alt-v"></i> Flip V</button>
<button class="btn btn-sm btn-dark rounded-pill px-3" id="apply-crop-btn">APPLY CROP</button>
</div>
</div>
<!-- Controls Area -->
<div class="col-lg-4 border-start">
<div class="p-4 overflow-auto" style="max-height: 80vh;">
<div class="col-lg-4 p-4 overflow-auto" style="max-height: 80vh;">
<ul class="nav nav-tabs border-0 mb-3 flex-nowrap overflow-auto" id="editor-tabs">
<li class="nav-item"><button class="nav-link active fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#filters-panel">ФИЛЬТРЫ</button></li>
<li class="nav-item"><button class="nav-link fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#transform-panel">ТРАНСФОРМ</button></li>
<li class="nav-item"><button class="nav-link fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#decor-panel">ДЕКОР</button></li>
<li class="nav-item"><button class="nav-link fw-bold text-dark border-0 small" data-bs-toggle="tab" data-bs-target="#ai-magic-panel">AI MAGIC</button></li>
</ul>
<div class="tab-content pt-2">
<!-- Filters -->
<div class="tab-pane fade show active" id="filters-panel">
<?php
$filters = [
'brightness' => ['label' => 'Яркость', 'min' => 0, 'max' => 200, 'val' => 100],
'contrast' => ['label' => 'Контраст', 'min' => 0, 'max' => 200, 'val' => 100],
'saturate' => ['label' => 'Насыщенность', 'min' => 0, 'max' => 200, 'val' => 100],
'blur' => ['label' => 'Размытие', 'min' => 0, 'max' => 20, 'val' => 0],
'hue-rotate' => ['label' => 'Оттенок', 'min' => 0, 'max' => 360, 'val' => 0],
'sepia' => ['label' => 'Сепия', 'min' => 0, 'max' => 100, 'val' => 0],
'grayscale' => ['label' => 'Ч/Б', 'min' => 0, 'max' => 100, 'val' => 0],
'vignette' => ['label' => 'Виньетка', 'min' => 0, 'max' => 100, 'val' => 0],
'noise' => ['label' => 'Шум', 'min' => 0, 'max' => 100, 'val' => 0]
];
foreach ($filters as $id => $f): ?>
<div class="mb-3">
<div class="d-flex justify-content-between">
<label class="form-label small fw-bold"><?php echo $f['label']; ?></label>
<span class="small fw-bold text-muted" id="val-<?php echo $id; ?>"><?php echo $f['val']; ?></span>
</div>
<input type="range" class="form-range filter-range" data-filter="<?php echo $id; ?>" min="<?php echo $f['min']; ?>" max="<?php echo $f['max']; ?>" value="<?php echo $f['val']; ?>">
</div>
<?php endforeach; ?>
</div>
<ul class="nav nav-pills mb-4 justify-content-center" id="editor-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="filters-tab" data-bs-toggle="pill" data-bs-target="#filters-panel" type="button" role="tab">Фильтры</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="ai-magic-tab" data-bs-toggle="pill" data-bs-target="#ai-magic-panel" type="button" role="tab">ИИ Магия</button>
</li>
</ul>
<div class="tab-content" id="editor-tab-content">
<!-- Filters Panel -->
<div class="tab-pane fade show active" id="filters-panel" role="tabpanel">
<h6 class="fw-bold mb-4 text-uppercase small letter-spacing-1">Настройки фильтров</h6>
<div class="filter-control mb-4">
<label>Яркость</label>
<input type="range" class="form-range filter-range" data-filter="brightness" min="0" max="200" value="100">
</div>
<div class="filter-control mb-4">
<label>Контраст</label>
<input type="range" class="form-range filter-range" data-filter="contrast" min="0" max="200" value="100">
</div>
<div class="filter-control mb-4">
<label>Насыщенность</label>
<input type="range" class="form-range filter-range" data-filter="saturate" min="0" max="200" value="100">
</div>
<div class="filter-control mb-4">
<label>Чёрно-белое</label>
<input type="range" class="form-range filter-range" data-filter="grayscale" min="0" max="100" value="0">
</div>
<div class="filter-control mb-4">
<label>Сепия</label>
<input type="range" class="form-range filter-range" data-filter="sepia" min="0" max="100" value="0">
</div>
<div class="filter-control mb-4">
<label>Инверсия</label>
<input type="range" class="form-range filter-range" data-filter="invert" min="0" max="100" value="0">
</div>
<div class="filter-control mb-4">
<label>Размытие</label>
<input type="range" class="form-range filter-range" data-filter="blur" min="0" max="20" value="0">
</div>
</div>
<!-- AI Magic Panel -->
<div class="tab-pane fade" id="ai-magic-panel" role="tabpanel">
<h6 class="fw-bold mb-3 text-uppercase small letter-spacing-1">Умное редактирование</h6>
<div class="ai-magic-box mb-4">
<label class="form-label small fw-bold mb-2">Что изменить на фото?</label>
<textarea class="form-control mb-2" id="ai-edit-prompt" rows="3" placeholder="Например: 'Добавь коту корону' или 'Сделай фон в стиле киберпанк'"></textarea>
<button class="btn btn-primary btn-sm w-100 fw-bold" id="apply-ai-magic">
<i class="fas fa-sparkles me-1"></i> Применить магию
</button>
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-dark btn-sm" id="remove-bg-btn">
<i class="fas fa-eraser me-1"></i> Удалить фон (ИИ)
</button>
<button class="btn btn-outline-dark btn-sm" id="upscale-btn">
<i class="fas fa-expand me-1"></i> Улучшить качество
</button>
</div>
<div class="alert alert-info mt-4 small border-0 py-2">
<i class="fas fa-lightbulb me-2"></i> ИИ анализирует фото и перерисовывает его согласно вашим пожеланиям.
<!-- Transform -->
<div class="tab-pane fade" id="transform-panel">
<div class="d-grid gap-3">
<button class="btn btn-nano w-100" id="start-crop-btn">
<i class="fas fa-crop-alt"></i> ИНСТРУМЕНТ ОБРЕЗКИ
</button>
<div class="row g-2">
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3" onclick="rotateLeft()">
<i class="fas fa-undo d-block mb-1"></i> -90°
</button>
</div>
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3" onclick="rotateRight()">
<i class="fas fa-redo d-block mb-1"></i> +90°
</button>
</div>
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3" onclick="flipH()">
<i class="fas fa-arrows-alt-h d-block mb-1"></i> Flip H
</button>
</div>
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3" onclick="flipV()">
<i class="fas fa-arrows-alt-v d-block mb-1"></i> Flip V
</button>
</div>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-grid gap-2">
<button class="btn btn-outline-danger btn-sm mb-2" id="reset-filters">
<i class="fas fa-undo me-1"></i> Сбросить всё
</button>
<button class="btn btn-dark fw-bold" id="save-edited-btn">
<i class="fas fa-download me-1"></i> Скачать результат
</button>
<!-- Decor -->
<div class="tab-pane fade" id="decor-panel">
<div class="mb-4">
<label class="form-label small fw-bold">РИСОВАНИЕ КИСТЬЮ</label>
<div class="d-flex gap-2 mb-2">
<button class="btn tool-btn flex-grow-1" id="brush-toggle"><i class="fas fa-paint-brush"></i> Кисть</button>
<input type="color" class="form-control form-control-color border-2 border-dark" id="brush-color" value="#FFDE59" title="Цвет кисти">
</div>
<input type="range" class="form-range" id="brush-size" min="1" max="100" value="10">
</div>
<div class="mb-4">
<label class="form-label small fw-bold">ТЕКСТ</label>
<div class="input-group mb-2">
<input type="text" id="text-input" class="form-control form-control-nano" placeholder="Ваш текст...">
<button class="btn btn-dark" id="add-text-btn"><i class="fas fa-plus"></i></button>
</div>
<div class="d-flex gap-2">
<select id="font-family" class="form-select form-control-nano py-1">
<option value="Montserrat">Montserrat</option>
<option value="Inter">Inter</option>
<option value="Arial">Arial</option>
<option value="Courier New">Monospace</option>
</select>
<input type="color" class="form-control form-control-color border-2 border-dark" id="text-color" value="#000000">
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold">СЛОИ (OBJECTS)</label>
<div class="d-flex gap-2">
<button class="btn btn-outline-dark btn-sm flex-grow-1" onclick="bringToFront()"><i class="fas fa-layer-group"></i> Вперёд</button>
<button class="btn btn-outline-dark btn-sm flex-grow-1" onclick="sendToBack()"><i class="fas fa-level-down-alt"></i> Назад</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteObject()"><i class="fas fa-trash"></i></button>
</div>
</div>
<div>
<label class="form-label small fw-bold">СТИКЕРЫ 🍌✨</label>
<div class="d-flex flex-wrap gap-2 p-2 bg-white rounded-3 border border-2 border-dark">
<div class="sticker-item" data-sticker="🍌">🍌</div>
<div class="sticker-item" data-sticker="🐒">🐒</div>
<div class="sticker-item" data-sticker="🌴">🌴</div>
<div class="sticker-item" data-sticker="🕶️">🕶️</div>
<div class="sticker-item" data-sticker="🔥">🔥</div>
<div class="sticker-item" data-sticker="❤️">❤️</div>
<div class="sticker-item" data-sticker=""></div>
<div class="sticker-item" data-sticker="🚀">🚀</div>
<div class="sticker-item" data-sticker="🎨">🎨</div>
<div class="sticker-item" data-sticker=""></div>
</div>
</div>
</div>
<!-- AI Magic -->
<div class="tab-pane fade" id="ai-magic-panel">
<div class="p-3 bg-yellow-soft rounded-4 border border-2 border-dark mb-3">
<label class="form-label small fw-bold">ЧТО ДОРИСОВАТЬ?</label>
<textarea class="form-control form-control-nano mb-2" id="ai-edit-prompt" rows="3" placeholder="Напр: Добавь солнечные очки..."></textarea>
<button class="btn btn-nano w-100" id="apply-ai-magic">ПРИМЕНИТЬ МАГИЮ</button>
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-dark btn-sm rounded-pill" id="remove-bg-btn"><i class="fas fa-user-slash me-1"></i> Удалить фон</button>
<button class="btn btn-outline-dark btn-sm rounded-pill" id="upscale-btn"><i class="fas fa-expand-arrows-alt me-1"></i> Улучшить (HD)</button>
</div>
</div>
</div>
<hr class="border-2 border-dark">
<div class="d-grid gap-2">
<button class="btn btn-outline-danger btn-sm rounded-pill" id="reset-editor"><i class="fas fa-undo"></i> СБРОСИТЬ ВСЁ</button>
<button class="btn btn-nano w-100 py-3 mt-2" id="save-edited-btn">
<i class="fas fa-download me-2"></i> СКАЧАТЬ PNG
</button>
<button class="btn btn-dark w-100 py-2 rounded-pill" id="save-to-gallery-btn">
<i class="fas fa-cloud-upload-alt me-2"></i> СОХРАНИТЬ В ГАЛЕРЕЮ
</button>
</div>
</div>
</div>
@ -396,73 +451,11 @@ $project_description = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Generate unique AI ph
</div>
</div>
<!-- Settings Modal -->
<div class="modal fade" id="settingsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold">Настройки API</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="mb-4">
<label class="form-label small fw-bold">Ваш X-RapidAPI-Key</label>
<div class="input-group">
<span class="input-group-text border-2"><i class="fas fa-key"></i></span>
<input type="password" class="form-control border-2" id="rapidapi-key-input" placeholder="Введите ваш ключ...">
</div>
<div class="form-text mt-2 text-muted">
Этот ключ хранится только в вашем браузере.
</div>
</div>
<div class="alert alert-light border">
<h6 class="fw-bold mb-3">Где получить бесплатный ключ для видео?</h6>
<p class="small mb-3">Зарегистрируйтесь на <a href="https://rapidapi.com/" target="_blank">RapidAPI</a> и подпишитесь на один из этих бесплатных планов (Free Tier):</p>
<div class="list-group list-group-flush small">
<a href="https://rapidapi.com/novita-ai-novita-ai-default/api/novita-ai" target="_blank" class="list-group-item list-group-item-action py-3 px-0 border-0">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1 fw-bold">1. Novita AI</h6>
<span class="badge bg-success">Рекомендуется</span>
</div>
<p class="mb-1">Мощная генерация видео и фото.</p>
</a>
<a href="https://rapidapi.com/moat-io-moat-io-default/api/text-to-video2" target="_blank" class="list-group-item list-group-item-action py-3 px-0 border-0">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1 fw-bold">2. Moat Text-to-Video</h6>
</div>
<p class="mb-1">Хорошая база бесплатных генераций в день.</p>
</a>
<a href="https://rapidapi.com/pictory-pictory-default/api/pictory" target="_blank" class="list-group-item list-group-item-action py-3 px-0 border-0">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1 fw-bold">3. Pictory AI</h6>
</div>
<p class="mb-1">Генерация видео из текста (более длинные ролики).</p>
</a>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-dark w-100 py-2 fw-bold" data-bs-dismiss="modal">Сохранить настройки</button>
</div>
</div>
</div>
</div>
<footer class="bg-white py-5 border-top mt-5">
<div class="container text-center">
<p class="text-muted mb-0">&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars($project_name); ?></p>
<div class="mt-2">
<span class="badge bg-light text-dark border">PHP <?php echo PHP_VERSION; ?></span>
<span class="badge bg-light text-dark border">Flux AI</span>
<span class="badge bg-light text-dark border">RapidAPI Ready</span>
</div>
</div>
<footer class="py-5 text-center">
<p class="small fw-bold">&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars($project_name); ?> 🍌</p>
</footer>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Main JS -->
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>