Cloud
This commit is contained in:
parent
4e986c09da
commit
cf734ceaa6
44
api.php
44
api.php
@ -3,12 +3,14 @@ header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
use Kunnu\Dropbox\Dropbox;
|
||||
use Kunnu\Dropbox\DropboxApp;
|
||||
use Kunnu\Dropbox\DropboxFile;
|
||||
use Kunnu\Dropbox\Exceptions\DropboxClientException;
|
||||
|
||||
$response = [];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
use Dropbox\Client;
|
||||
use Dropbox\Exception;
|
||||
|
||||
switch ($action) {
|
||||
case 'save':
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
@ -64,26 +66,6 @@ switch ($action) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'get_stream':
|
||||
try {
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (empty($id)) {
|
||||
http_response_code(400);
|
||||
$response['error'] = 'O ID do stream é obrigatório.';
|
||||
} else {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT id, url, filename, status, created_at, progress FROM streams WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$stream = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$response['success'] = true;
|
||||
$response['stream'] = $stream;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
$response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'convert_to_mp4':
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
@ -103,7 +85,6 @@ switch ($action) {
|
||||
http_response_code(404);
|
||||
$response['error'] = 'Stream não encontrado.';
|
||||
} else {
|
||||
// Get video duration using ffprobe
|
||||
$ffprobe_command = [
|
||||
'ffprobe',
|
||||
'-v', 'error',
|
||||
@ -197,16 +178,16 @@ switch ($action) {
|
||||
$last_match = end($matches[1]);
|
||||
|
||||
if ($last_match) {
|
||||
$processed_ms = (float)$last_match / 1000000; // a unidade está em microsegundos
|
||||
$processed_ms = (float)$last_match / 1000000;
|
||||
$duration = (float)$stream['duration'];
|
||||
$progress = $duration > 0 ? round(($processed_ms / $duration) * 100) : 0;
|
||||
$progress = min(100, $progress); // Garante que o progresso não passe de 100
|
||||
$progress = min(100, $progress);
|
||||
|
||||
$pdo->prepare("UPDATE streams SET progress = :progress WHERE id = :id")->execute([':progress' => $progress, ':id' => $id]);
|
||||
|
||||
if ($progress == 100) {
|
||||
$pdo->prepare("UPDATE streams SET status = 'completed', converted_path = :converted_path WHERE id = :id")->execute([':converted_path' => $output_filename, ':id' => $id]);
|
||||
unlink($progress_log_path); // Limpa o arquivo de log
|
||||
if (file_exists($progress_log_path)) unlink($progress_log_path);
|
||||
}
|
||||
|
||||
$response['progress'] = $progress;
|
||||
@ -263,11 +244,12 @@ switch ($action) {
|
||||
break;
|
||||
}
|
||||
|
||||
$app = new \Dropbox\App("", "", $token);
|
||||
$dropbox = new \Dropbox\Client($app);
|
||||
$app = new DropboxApp("", "", $token);
|
||||
$dropbox = new Dropbox($app);
|
||||
|
||||
$dropboxFileName = '/' . basename($filePath);
|
||||
$file = $dropbox->upload($filePath, $dropboxFileName, ['autorename' => true]);
|
||||
$dropboxFile = new DropboxFile($filePath);
|
||||
$file = $dropbox->upload($dropboxFile, $dropboxFileName, ['autorename' => true]);
|
||||
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'Arquivo enviado com sucesso para o Dropbox!';
|
||||
@ -276,7 +258,7 @@ switch ($action) {
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
$response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
|
||||
} catch (\Dropbox\Exception $e) {
|
||||
} catch (DropboxClientException $e) {
|
||||
http_response_code(500);
|
||||
$response['error'] = 'Erro no Dropbox: ' . $e->getMessage();
|
||||
} catch (Exception $e) {
|
||||
|
||||
64
api/chat.php
Normal file
64
api/chat.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$message = $input['message'] ?? '';
|
||||
|
||||
if (empty($message)) {
|
||||
echo json_encode(['reply' => "I didn't catch that. Could you repeat?"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Fetch Knowledge Base (FAQs)
|
||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
||||
foreach ($faqs as $faq) {
|
||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
||||
}
|
||||
|
||||
// 2. Construct Prompt for AI
|
||||
$systemPrompt = "You are a helpful, friendly AI assistant for this website. " .
|
||||
"Use the provided Knowledge Base to answer user questions accurately. " .
|
||||
"If the answer is found in the Knowledge Base, rephrase it naturally. " .
|
||||
"If the answer is NOT in the Knowledge Base, use your general knowledge to help, " .
|
||||
"but politely mention that you don't have specific information about that if it seems like a site-specific question. " .
|
||||
"Keep answers concise and professional.\n\n" .
|
||||
$knowledgeBase;
|
||||
|
||||
// 3. Call AI API
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $message],
|
||||
]
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$aiReply = LocalAIApi::extractText($response);
|
||||
|
||||
// 4. Save to Database
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
|
||||
$stmt->execute([$message, $aiReply]);
|
||||
} catch (Exception $e) {
|
||||
error_log("DB Save Error: " . $e->getMessage());
|
||||
// Continue even if save fails, so the user still gets a reply
|
||||
}
|
||||
|
||||
echo json_encode(['reply' => $aiReply]);
|
||||
} else {
|
||||
// Fallback if AI fails
|
||||
error_log("AI Error: " . ($response['error'] ?? 'Unknown'));
|
||||
echo json_encode(['reply' => "I'm having trouble connecting to my brain right now. Please try again later."]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Chat Error: " . $e->getMessage());
|
||||
echo json_encode(['reply' => "An internal error occurred."]);
|
||||
}
|
||||
91
api/telegram_webhook.php
Normal file
91
api/telegram_webhook.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
// Get Telegram Update
|
||||
$content = file_get_contents("php://input");
|
||||
$update = json_decode($content, true);
|
||||
|
||||
if (!$update || !isset($update['message'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$message = $update['message'];
|
||||
$chatId = $message['chat']['id'];
|
||||
$text = $message['text'] ?? '';
|
||||
|
||||
if (empty($text)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get Telegram Token from DB
|
||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
||||
$token = $stmt->fetchColumn();
|
||||
|
||||
if (!$token) {
|
||||
error_log("Telegram Error: No bot token found in settings.");
|
||||
exit;
|
||||
}
|
||||
|
||||
function sendTelegramMessage($chatId, $text, $token) {
|
||||
$url = "https://api.telegram.org/bot$token/sendMessage";
|
||||
$data = [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'parse_mode' => 'Markdown'
|
||||
];
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => http_build_query($data),
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($options);
|
||||
return file_get_contents($url, false, $context);
|
||||
}
|
||||
|
||||
// Process with AI (Similar logic to api/chat.php)
|
||||
try {
|
||||
// 1. Fetch Knowledge Base
|
||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
||||
foreach ($faqs as $faq) {
|
||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
||||
}
|
||||
|
||||
$systemPrompt = "You are a helpful AI assistant integrated with Telegram. " .
|
||||
"Use the provided Knowledge Base to answer user questions. " .
|
||||
"Keep answers concise for mobile reading. Use Markdown for formatting.\n\n" .
|
||||
$knowledgeBase;
|
||||
|
||||
// 2. Call AI
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $text],
|
||||
]
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$aiReply = LocalAIApi::extractText($response);
|
||||
|
||||
// 3. Save History
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
|
||||
$stmt->execute(["[Telegram] " . $text, $aiReply]);
|
||||
} catch (Exception $e) {}
|
||||
|
||||
// 4. Send back to Telegram
|
||||
sendTelegramMessage($chatId, $aiReply, $token);
|
||||
} else {
|
||||
sendTelegramMessage($chatId, "I'm sorry, I encountered an error processing your request.", $token);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Telegram Webhook Error: " . $e->getMessage());
|
||||
}
|
||||
426
assets/css/app.css
Normal file
426
assets/css/app.css
Normal file
@ -0,0 +1,426 @@
|
||||
:root {
|
||||
--background: #f8fafc;
|
||||
--surface: rgba(255, 255, 255, 0.7);
|
||||
--primary: #6366f1;
|
||||
--primary-light: #818cf8;
|
||||
--secondary: #ec4899;
|
||||
--accent: #06b6d4;
|
||||
--text-main: #1e293b;
|
||||
--text-muted: #64748b;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--sidebar-width: 280px;
|
||||
--glass-border: rgba(255, 255, 255, 0.4);
|
||||
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--text-main);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Background Blobs */
|
||||
.bg-blobs {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
filter: blur(80px);
|
||||
border-radius: 50%;
|
||||
opacity: 0.4;
|
||||
animation: blob-move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: var(--primary-light);
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: var(--secondary);
|
||||
bottom: -150px;
|
||||
left: -150px;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: var(--accent);
|
||||
top: 40%;
|
||||
left: 20%;
|
||||
animation-delay: -10s;
|
||||
}
|
||||
|
||||
@keyframes blob-move {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(100px, 50px) scale(1.1); }
|
||||
66% { transform: translate(-50px, 150px) scale(0.9); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar Styling */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: var(--surface);
|
||||
backdrop-filter: blur(20px);
|
||||
border-right: 1px solid var(--glass-border);
|
||||
padding: 2.5rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.875rem 1.25rem;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sidebar nav a i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.sidebar nav a:hover, .sidebar nav a.active {
|
||||
background: white;
|
||||
color: var(--primary);
|
||||
box-shadow: var(--shadow);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.sidebar nav a.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main Content Styling */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
padding: 2.5rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
header h2 {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Cards & Grid */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 24px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 1.75rem;
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Form Styling */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
background: white;
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-radius: 16px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 1rem 1.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #fee2e2;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Table Styling */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.75rem;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: white;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr td:first-child { border-radius: 16px 0 0 16px; }
|
||||
tr td:last-child { border-radius: 0 16px 16px 0; }
|
||||
|
||||
.status-badge {
|
||||
padding: 0.375rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-pending { background: #fef3c7; color: #92400e; }
|
||||
.status-converting { background: #dbeafe; color: #1e40af; }
|
||||
.status-completed { background: #d1fae5; color: #065f46; }
|
||||
.status-failed { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar-container {
|
||||
width: 120px;
|
||||
height: 8px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: 9999px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* Video Preview */
|
||||
video#preview {
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 600;
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
border-left: 6px solid var(--primary);
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toast-success { border-color: var(--success); }
|
||||
.toast-error { border-color: var(--error); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.sidebar h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.sidebar nav {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.main-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
302
assets/css/custom.css
Normal file
302
assets/css/custom.css
Normal file
@ -0,0 +1,302 @@
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
padding: 0.85rem 1.1rem;
|
||||
border-radius: 16px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
.message.visitor {
|
||||
align-self: flex-end;
|
||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
align-self: flex-start;
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-input-area form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-input-area input {
|
||||
flex: 1;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
}
|
||||
|
||||
.chat-input-area button {
|
||||
background: #212529;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area button:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
.bg-animations {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
background: rgba(238, 119, 82, 0.4);
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
background: rgba(35, 166, 213, 0.4);
|
||||
animation-delay: -7s;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
top: 40%;
|
||||
left: 30%;
|
||||
background: rgba(231, 60, 126, 0.3);
|
||||
animation-delay: -14s;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
||||
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
|
||||
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
|
||||
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
|
||||
}
|
||||
|
||||
.admin-link {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-link:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Admin Styles */
|
||||
.admin-container {
|
||||
max-width: 900px;
|
||||
margin: 3rem auto;
|
||||
padding: 2.5rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.admin-container h1 {
|
||||
margin-top: 0;
|
||||
color: #212529;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table tr td:first-child { border-radius: 12px 0 0 12px; }
|
||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
}
|
||||
191
assets/js/app.js
Normal file
191
assets/js/app.js
Normal file
@ -0,0 +1,191 @@
|
||||
let hls = null;
|
||||
|
||||
function playUrlInPreviewer(url) {
|
||||
const previewVideo = document.getElementById('preview');
|
||||
if (!url || !previewVideo) return;
|
||||
|
||||
if (hls) {
|
||||
hls.destroy();
|
||||
}
|
||||
|
||||
if (url.endsWith('.m3u8')) {
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls();
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(previewVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
|
||||
});
|
||||
} else if (previewVideo.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
previewVideo.src = url;
|
||||
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
|
||||
}
|
||||
} else {
|
||||
previewVideo.src = url;
|
||||
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
|
||||
}
|
||||
}
|
||||
|
||||
function playVideo(url) {
|
||||
const streamUrlInput = document.getElementById('stream-url');
|
||||
if(streamUrlInput) {
|
||||
streamUrlInput.value = url;
|
||||
}
|
||||
playUrlInPreviewer(url);
|
||||
|
||||
const videoSection = document.querySelector('.video-section');
|
||||
if(videoSection){
|
||||
videoSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
async function updateConversionProgress() {
|
||||
const convertingRows = document.querySelectorAll('tr[data-status="converting"]');
|
||||
if (convertingRows.length === 0) return;
|
||||
|
||||
for (const row of convertingRows) {
|
||||
const streamId = row.dataset.streamId;
|
||||
if (!streamId) continue;
|
||||
|
||||
try {
|
||||
const response = await fetch(`api.php?action=get_conversion_progress&id=${streamId}`);
|
||||
if (!response.ok) continue;
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.progress !== undefined) {
|
||||
const progressBar = row.querySelector('.progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${result.progress}%`;
|
||||
}
|
||||
|
||||
if (result.progress >= 100) {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao atualizar progresso:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToDropbox(id, button) {
|
||||
const tokenInput = document.getElementById('dropbox-token');
|
||||
const token = tokenInput ? tokenInput.value : '';
|
||||
if (!token) {
|
||||
showToast('Por favor, insira seu token de acesso do Dropbox na página do conversor.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = '...';
|
||||
showToast('Enviando para o Dropbox...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=send_to_dropbox', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, token }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Erro ao enviar para o Dropbox');
|
||||
|
||||
showToast(result.message, 'success');
|
||||
button.innerHTML = '✅';
|
||||
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalHtml;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVideo(id, button) {
|
||||
if (!confirm('Deseja deletar este vídeo?')) return;
|
||||
|
||||
button.disabled = true;
|
||||
showToast('Apagando...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Erro ao apagar');
|
||||
|
||||
showToast(result.message, 'success');
|
||||
button.closest('tr').remove();
|
||||
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => toast.classList.add('show'), 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 400);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const streamUrlInput = document.getElementById('stream-url');
|
||||
if (streamUrlInput && streamUrlInput.value) {
|
||||
playUrlInPreviewer(streamUrlInput.value);
|
||||
}
|
||||
|
||||
if(streamUrlInput) {
|
||||
streamUrlInput.addEventListener('input', () => {
|
||||
playUrlInPreviewer(streamUrlInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
if(saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const url = document.getElementById('stream-url').value;
|
||||
const filename = document.getElementById('filename').value;
|
||||
const token = document.getElementById('dropbox-token').value;
|
||||
|
||||
if (!url || !filename) {
|
||||
showToast('Preencha a URL e o nome do arquivo.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = 'Salvando...';
|
||||
|
||||
try {
|
||||
const saveResponse = await fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'save', url, filename, token }),
|
||||
});
|
||||
const saveResult = await saveResponse.json();
|
||||
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
|
||||
|
||||
showToast(saveResult.message, 'success');
|
||||
setTimeout(() => location.href = '?page=converter', 1500);
|
||||
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '💾 Salvar e Converter';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateConversionProgress();
|
||||
setInterval(updateConversionProgress, 5000);
|
||||
});
|
||||
39
assets/js/main.js
Normal file
39
assets/js/main.js
Normal file
@ -0,0 +1,39 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
});
|
||||
});
|
||||
207
cloud.php
207
cloud.php
@ -1,46 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My Video Cloud</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.video-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.video-list-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<a href="/" class="btn btn-secondary mb-4">Back to Dashboard</a>
|
||||
<h1 class="mb-4">My Video Cloud</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<video id="videoPlayer" width="100%" controls autoplay>
|
||||
<source src="" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>Playlist</h4>
|
||||
<ul class="list-group" id="videoList">
|
||||
<?php
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
use Dropbox\Client;
|
||||
use Dropbox\App;
|
||||
use Dropbox\Exception;
|
||||
use Kunnu\Dropbox\Dropbox;
|
||||
use Kunnu\Dropbox\DropboxApp;
|
||||
use Kunnu\Dropbox\Models\FileMetadata;
|
||||
|
||||
// Fetch the latest dropbox token from the database
|
||||
try {
|
||||
@ -52,86 +16,177 @@
|
||||
$token = null;
|
||||
error_log("Database error fetching token: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Minha Nuvem - CloudStream</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/app.css?v=<?php echo time(); ?>">
|
||||
<style>
|
||||
.video-list-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border: 1px solid var(--glass-border);
|
||||
list-style: none;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.video-list-item:hover {
|
||||
transform: translateX(5px);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.video-list-item.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.cloud-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.cloud-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="bg-blobs">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<aside class="sidebar">
|
||||
<h1>🎬 CloudStream</h1>
|
||||
<nav>
|
||||
<a href="index.php?page=dashboard">📊 Painel</a>
|
||||
<a href="index.php?page=converter">🔄 Conversor</a>
|
||||
<a href="cloud.php" class="active">☁️ Minha Nuvem</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h2>Minha Nuvem Dropbox</h2>
|
||||
<p>Gerencie e assista seus vídeos salvos diretamente da nuvem.</p>
|
||||
</header>
|
||||
|
||||
<div class="cloud-grid">
|
||||
<div class="video-player-section">
|
||||
<div class="card" style="padding: 1rem;">
|
||||
<video id="videoPlayer" controls autoplay style="width: 100%; border-radius: 16px; aspect-ratio: 16/9; background: #000;">
|
||||
<source src="" type="video/mp4">
|
||||
Seu navegador não suporta a tag de vídeo.
|
||||
</video>
|
||||
<div id="videoTitle" style="margin-top: 1rem; font-weight: 700; font-size: 1.25rem; color: var(--text-main);">Selecione um vídeo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-section">
|
||||
<div class="card">
|
||||
<h3 style="margin-bottom: 1.5rem; font-weight: 700;">Playlist na Nuvem</h3>
|
||||
<ul id="videoList" style="padding: 0;">
|
||||
<?php
|
||||
if ($token) {
|
||||
try {
|
||||
$app = new App("", "", $token);
|
||||
$dropbox = new Client($app);
|
||||
$app = new DropboxApp("", "", $token);
|
||||
$dropbox = new Dropbox($app);
|
||||
|
||||
$files = $dropbox->listFolder('/');
|
||||
$items = $files->getItems();
|
||||
$listFolderContents = $dropbox->listFolder('/');
|
||||
$items = $listFolderContents->getItems();
|
||||
|
||||
if (count($items) > 0) {
|
||||
$foundVideos = 0;
|
||||
foreach ($items as $item) {
|
||||
$metadata = $item->getMetadata();
|
||||
if ($metadata instanceof \Dropbox\Models\FileMetadata) {
|
||||
// Check if it's a video file based on extension
|
||||
if ($item instanceof FileMetadata) {
|
||||
$filename = strtolower($item->getName());
|
||||
$videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
|
||||
$filename = strtolower($metadata->getName());
|
||||
$shouldDisplay = false;
|
||||
$is_video = false;
|
||||
foreach($videoExtensions as $ext) {
|
||||
if (str_ends_with($filename, $ext)) {
|
||||
$shouldDisplay = true;
|
||||
$is_video = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldDisplay) {
|
||||
if ($is_video) {
|
||||
try {
|
||||
// Get a temporary link for the file
|
||||
$temporaryLink = $dropbox->getTemporaryLink($metadata->getPathLower());
|
||||
$temporaryLink = $dropbox->getTemporaryLink($item->getPathLower());
|
||||
$link = $temporaryLink->getLink();
|
||||
echo '<li class="list-group-item video-list-item" data-video-src="' . htmlspecialchars($link) . '">' . htmlspecialchars($metadata->getName()) . '</li>';
|
||||
} catch (Exception $e) {
|
||||
// Could not get a temporary link for some reason
|
||||
error_log("Dropbox API error getting temporary link: " . $e->getMessage());
|
||||
// Optionally, display an error for this specific file
|
||||
echo '<li class="list-group-item disabled" title="Could not load this video.">' . htmlspecialchars($metadata->getName()) . ' (Error)</li>';
|
||||
echo '<li class="video-list-item" data-video-src="' . htmlspecialchars($link) . '" data-video-name="' . htmlspecialchars($item->getName()) . '">
|
||||
<span>🎥</span> ' . htmlspecialchars($item->getName()) . '
|
||||
</li>';
|
||||
$foundVideos++;
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<li class="list-group-item">No videos found in your Dropbox.</li>';
|
||||
|
||||
if ($foundVideos === 0) {
|
||||
echo '<li style="list-style:none; color: var(--text-muted); text-align: center; padding: 2rem;">Nenhum vídeo encontrado no Dropbox.</li>';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Dropbox API error: " . $e->getMessage());
|
||||
if(str_contains($e->getMessage(), 'invalid_access_token')) {
|
||||
echo '<li class="list-group-item">Error connecting to Dropbox: The access token is invalid. Please go to the converter page and re-enter a valid token.</li>';
|
||||
} else {
|
||||
echo '<li class="list-group-item">Error connecting to Dropbox.</li>';
|
||||
}
|
||||
echo '<li style="list-style:none; color: var(--error); padding: 1rem; background: #fee2e2; border-radius: 12px;">Erro ao conectar com Dropbox. Verifique o token no conversor.</li>';
|
||||
}
|
||||
} else {
|
||||
echo '<li class="list-group-item">Dropbox is not configured. Please go to the converter page, provide a token, and upload a video first.</li>';
|
||||
echo '<li style="list-style:none; color: var(--text-muted); text-align: center; padding: 2rem;">Dropbox não configurado. Adicione um token no conversor primeiro.</li>';
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
const videoListItems = document.querySelectorAll('.video-list-item');
|
||||
const videoSource = videoPlayer.querySelector('source');
|
||||
const videoTitle = document.getElementById('videoTitle');
|
||||
|
||||
function playSelectedVideo(item) {
|
||||
const videoSrc = item.getAttribute('data-video-src');
|
||||
const name = item.getAttribute('data-video-name');
|
||||
|
||||
videoListItems.forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
videoTitle.textContent = name;
|
||||
videoSource.setAttribute('src', videoSrc);
|
||||
videoPlayer.load();
|
||||
videoPlayer.play().catch(e => console.log("Autoplay blocked"));
|
||||
}
|
||||
|
||||
if (videoListItems.length > 0) {
|
||||
const firstVideoSrc = videoListItems[0].getAttribute('data-video-src');
|
||||
videoSource.setAttribute('src', firstVideoSrc);
|
||||
videoPlayer.load();
|
||||
playSelectedVideo(videoListItems[0]);
|
||||
}
|
||||
|
||||
videoListItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const videoSrc = this.getAttribute('data-video-src');
|
||||
videoSource.setAttribute('src', videoSrc);
|
||||
videoPlayer.load();
|
||||
videoPlayer.play();
|
||||
playSelectedVideo(this);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
684
index.php
684
index.php
@ -1,4 +1,8 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$page = $_GET['page'] ?? 'dashboard';
|
||||
|
||||
function get_video_stats() {
|
||||
@ -6,25 +10,21 @@ function get_video_stats() {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT status, COUNT(*) as count FROM streams GROUP BY status");
|
||||
$stats = [
|
||||
'total' => 0,
|
||||
'completed' => 0,
|
||||
'converting' => 0,
|
||||
'pending' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
$stats = ['total' => 0, 'completed' => 0, 'converting' => 0, 'pending' => 0, 'failed' => 0];
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$stats[strtolower($row['status'])] = $row['count'];
|
||||
$stats['total'] += $row['count'];
|
||||
$s = strtolower($row['status']);
|
||||
if (isset($stats[$s])) {
|
||||
$stats[$s] = (int)$row['count'];
|
||||
}
|
||||
$stats['total'] += (int)$row['count'];
|
||||
}
|
||||
return $stats;
|
||||
} catch (PDOException $e) {
|
||||
return null;
|
||||
} catch (Exception $e) {
|
||||
return ['total' => 0, 'completed' => 0, 'converting' => 0, 'pending' => 0, 'failed' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
$video_stats = get_video_stats();
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@ -32,327 +32,24 @@ $video_stats = get_video_stats();
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CloudStream - Painel</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--background: #121212;
|
||||
--surface: #1E1E1E;
|
||||
--primary: #BB86FC;
|
||||
--primary-variant: #3700B3;
|
||||
--secondary: #03DAC6;
|
||||
--on-background: #FFFFFF;
|
||||
--on-surface: #E0E0E0;
|
||||
--success: #4CAF50;
|
||||
--error: #CF6679;
|
||||
--warning: #FB8C00;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--on-background);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: var(--surface);
|
||||
padding: 2rem 1rem;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
letter-spacing: -1px;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sidebar nav a {
|
||||
display: block;
|
||||
padding: 0.8rem 1rem;
|
||||
color: var(--on-surface);
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar nav a.active, .sidebar nav a:hover {
|
||||
background-color: var(--primary);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--on-background);
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--on-surface);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--surface);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* STATS CARDS */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stat-card h3 {
|
||||
font-size: 1rem;
|
||||
color: var(--on-surface);
|
||||
opacity: 0.8;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--on-background);
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(187, 134, 252, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.8rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #a766f8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--secondary);
|
||||
border: 2px solid var(--secondary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #b04b5c;
|
||||
}
|
||||
|
||||
.btn-dropbox {
|
||||
background-color: #0061FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-dropbox:hover {
|
||||
background-color: #0052d9;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.video-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
#preview {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.file-list h2 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--on-surface);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pending { background-color: var(--warning); color: #000; }
|
||||
.status-converting { background-color: #2196F3; color: #fff; }
|
||||
.status-completed { background-color: var(--success); color: #fff; }
|
||||
.status-failed { background-color: var(--error); color: #fff; }
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background-color: var(--surface);
|
||||
color: var(--on-background);
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
border-left: 4px solid;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
z-index: 1000;
|
||||
}
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.toast-success { border-color: var(--success); }
|
||||
.toast-error { border-color: var(--error); }
|
||||
.toast-info { border-color: #2196F3; }
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--secondary);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="assets/css/app.css?v=<?= time() ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<aside class="sidebar">
|
||||
<h1>CloudStream</h1>
|
||||
<h1>🎬 CloudStream</h1>
|
||||
<nav>
|
||||
<a href="?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">📊 Painel</a>
|
||||
<a href="?page=converter" class="<?= $page === 'converter' ? 'active' : '' ?>">🔄 Conversor</a>
|
||||
<a href="cloud.php">☁️ My Video Cloud</a>
|
||||
<a href="index.php?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">📊 Painel</a>
|
||||
<a href="index.php?page=converter" class="<?= $page === 'converter' ? 'active' : '' ?>">🔄 Conversor</a>
|
||||
<a href="cloud.php">☁️ Minha Nuvem</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@ -360,152 +57,144 @@ $video_stats = get_video_stats();
|
||||
<div class="container">
|
||||
<?php if ($page === 'dashboard'): ?>
|
||||
<header>
|
||||
<h2>Painel de Controle</h2>
|
||||
<p>Visão geral do seu sistema de vídeos.</p>
|
||||
<h2>Olá! 👋</h2>
|
||||
<p>Aqui está o resumo da sua biblioteca de vídeos.</p>
|
||||
</header>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>Total de Vídeos</h3>
|
||||
<div class="value"><?= htmlspecialchars($video_stats['total'] ?? 0) ?></div>
|
||||
<div class="value"><?= $video_stats['total'] ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Concluídos</h3>
|
||||
<div class="value" style="color: var(--success)"><?= htmlspecialchars($video_stats['completed'] ?? 0) ?></div>
|
||||
<div class="value" style="color:var(--success)"><?= $video_stats['completed'] ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Em Conversão</h3>
|
||||
<div class="value" style="color: #2196F3"><?= htmlspecialchars($video_stats['converting'] ?? 0) ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Falhas</h3>
|
||||
<div class="value" style="color: var(--error)"><?= htmlspecialchars($video_stats['failed'] ?? 0) ?></div>
|
||||
<h3>Em Processamento</h3>
|
||||
<div class="value" style="color:var(--accent)"><?= $video_stats['converting'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card file-list">
|
||||
<h2>Últimos Arquivos</h2>
|
||||
<div class="card">
|
||||
<h3>Atividade Recente</h3>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Nome do Arquivo</th>
|
||||
<th>Status</th>
|
||||
<th>Criação</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
try {
|
||||
require_once 'db/config.php';
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT id, filename, status, created_at FROM streams ORDER BY created_at DESC LIMIT 5");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$status = htmlspecialchars($row['status']);
|
||||
$statusClass = 'status-' . strtolower($status);
|
||||
|
||||
$stmt = $pdo->query("SELECT filename, status, created_at FROM streams ORDER BY created_at DESC LIMIT 5");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (empty($rows)) {
|
||||
echo "<tr><td colspan='3' style='text-align:center;padding:2rem;color:var(--text-muted)'>Nenhum vídeo encontrado.</td></tr>";
|
||||
} else {
|
||||
foreach ($rows as $row) {
|
||||
$sc = 'status-' . strtolower($row['status']);
|
||||
echo "<tr>";
|
||||
echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
|
||||
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
|
||||
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
|
||||
echo "<td><span class='status-badge $sc'>" . htmlspecialchars($row['status']) . "</span></td>";
|
||||
echo "<td style='color:var(--text-muted)'>" . date('d/m/Y H:i', strtotime($row['created_at'])) . "</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
if ($stmt->rowCount() === 0) {
|
||||
echo "<tr><td colspan='3' style='text-align: center; padding: 2rem;'>Nenhum arquivo recente.</td></tr>";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "<tr><td colspan='3' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos.</td></tr>";
|
||||
} catch (Exception $e) {
|
||||
echo "<tr><td colspan='3'>Erro ao carregar dados: " . htmlspecialchars($e->getMessage()) . "</td></tr>";
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($page === 'converter'): ?>
|
||||
<header>
|
||||
<h2>Conversor de Vídeo</h2>
|
||||
<p>Converta seus vídeos M3U8 para MP4 de forma simples e rápida.</p>
|
||||
<h2>🔄 Conversor HLS</h2>
|
||||
<p>Adicione um link de transmissão para converter e salvar.</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-grid">
|
||||
<div class="input-group" style="grid-column: 1 / -1;">
|
||||
<label for="stream-url">URL do Stream (M3U8 ou MP4)</label>
|
||||
<input type="url" id="stream-url" placeholder="https://exemplo.com/stream.m3u8" value="https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8">
|
||||
<label>URL do Stream (M3U8)</label>
|
||||
<input type="url" id="stream-url" placeholder="https://exemplo.com/live/playlist.m3u8" value="https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="filename">Nome do Arquivo (sem extensão)</label>
|
||||
<input type="text" id="filename" placeholder="meu-video-incrivel" value="bigbuckbunny-clip">
|
||||
<label>Nome do Arquivo de Saída</label>
|
||||
<input type="text" id="filename" value="Video_<?= date('His') ?>">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="dropbox-token">Token do Dropbox (Opcional)</label>
|
||||
<input type="password" id="dropbox-token" placeholder="Cole seu token de acesso aqui">
|
||||
<label>Dropbox Token (Opcional)</label>
|
||||
<input type="password" id="dropbox-token" placeholder="Seu token do Dropbox">
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right; margin-top: 1.5rem;">
|
||||
<button id="save-btn" class="btn btn-primary">💾 Salvar e Converter</button>
|
||||
<div style="margin-top: 2rem; text-align: right;">
|
||||
<button id="save-btn" class="btn btn-primary">🚀 Iniciar Conversão</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-section card">
|
||||
<h3>Pré-visualização</h3>
|
||||
<div class="video-section card" id="preview-section" style="display:none;">
|
||||
<h3>📺 Preview</h3>
|
||||
<video id="preview" controls></video>
|
||||
</div>
|
||||
|
||||
<div class="file-list card">
|
||||
<h2>Meus Arquivos</h2>
|
||||
<div class="card">
|
||||
<h3>Minha Biblioteca</h3>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Arquivo</th>
|
||||
<th>Status</th>
|
||||
<th>Criação</th>
|
||||
<th>Progresso</th>
|
||||
<th style="text-align:right;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="streams-table-body">
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
try {
|
||||
require_once 'db/config.php';
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT id, filename, status, created_at, url, converted_path FROM streams ORDER BY created_at DESC");
|
||||
$stmt = $pdo->query("SELECT id, filename, status, url, converted_path, progress FROM streams ORDER BY created_at DESC");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$status = htmlspecialchars($row['status']);
|
||||
$statusClass = 'status-' . strtolower($status);
|
||||
|
||||
echo "<tr data-stream-id='" . $row['id'] . "' data-status='" . $status . "'>";
|
||||
echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
|
||||
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
|
||||
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
|
||||
$sc = 'status-' . strtolower($row['status']);
|
||||
echo "<tr data-stream-id='" . $row['id'] . "' data-status='" . htmlspecialchars($row['status']) . "'>";
|
||||
echo "<td style='font-weight:600;'>" . htmlspecialchars($row['filename']) . "</td>";
|
||||
echo "<td><span class='status-badge $sc'>" . htmlspecialchars($row['status']) . "</span></td>";
|
||||
echo "<td>";
|
||||
if ($status === 'converting') {
|
||||
echo '<div class="progress-bar-container"><div class="progress-bar" style="width:' . ($row['progress'] ?? 0) . '%;"></div></div>';
|
||||
if ($row['status'] === 'converting') {
|
||||
echo "<div class='progress-bar-container'><div class='progress-bar' style='width:" . $row['progress'] . "%'></div></div>";
|
||||
}
|
||||
echo "</td>";
|
||||
echo "<td class='actions' style='justify-content: flex-end;'>";
|
||||
echo "<td style='text-align:right;'>";
|
||||
echo "<div style='display:flex;gap:0.5rem;justify-content:flex-end;'>";
|
||||
|
||||
$playUrl = ($status === 'completed' && !empty($row['converted_path']))
|
||||
? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES)
|
||||
: htmlspecialchars($row['url'], ENT_QUOTES);
|
||||
echo '<button class="btn btn-secondary" onclick="playVideo(\'" . $playUrl . "\')">▶️ Play</button>';
|
||||
$playUrl = ($row['status'] === 'completed' && !empty($row['converted_path'])) ? 'videos/'.$row['converted_path'] : $row['url'];
|
||||
echo "<button class='btn btn-secondary' onclick=\"playVideo('" . htmlspecialchars($playUrl, ENT_QUOTES) . "')\" title='Play'>▶️</button>";
|
||||
|
||||
if ($status === 'completed') {
|
||||
echo '<button class="btn btn-dropbox" onclick="sendToDropbox(' . $row['id'] . ', this)" title="Enviar para Dropbox">☁️</button>';
|
||||
if ($row['status'] === 'completed') {
|
||||
echo "<button class='btn btn-secondary' onclick='sendToDropbox(" . $row['id'] . ", this)' title='Enviar para Dropbox'>☁️</button>";
|
||||
}
|
||||
|
||||
echo '<button class="btn btn-danger" onclick="deleteVideo(' . $row['id'] . ', this)" title="Deletar">🗑️</button>';
|
||||
|
||||
echo "<button class='btn btn-danger' onclick='deleteVideo(" . $row['id'] . ", this)' title='Excluir'>🗑️</button>";
|
||||
echo "</div>";
|
||||
echo "</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
if ($stmt->rowCount() === 0) {
|
||||
echo "<tr><td colspan='5' style='text-align: center; padding: 2rem; color: var(--on-surface); opacity: 0.7;'>Nenhum arquivo encontrado. Adicione um stream para começar.</td></tr>";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "<tr><td colspan='5' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos: " . $e->getMessage() . "</td></tr>";
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
@ -513,209 +202,6 @@ $video_stats = get_video_stats();
|
||||
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script>
|
||||
let hls = null;
|
||||
|
||||
function playUrlInPreviewer(url) {
|
||||
const previewVideo = document.getElementById('preview');
|
||||
if (!url || !previewVideo) return;
|
||||
|
||||
if (hls) {
|
||||
hls.destroy();
|
||||
}
|
||||
|
||||
if (url.endsWith('.m3u8')) {
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls();
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(previewVideo);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
|
||||
});
|
||||
} else if (previewVideo.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
previewVideo.src = url;
|
||||
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
|
||||
}
|
||||
} else {
|
||||
previewVideo.src = url;
|
||||
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
|
||||
}
|
||||
}
|
||||
|
||||
function playVideo(url) {
|
||||
const streamUrlInput = document.getElementById('stream-url');
|
||||
if(streamUrlInput) {
|
||||
streamUrlInput.value = url;
|
||||
}
|
||||
playUrlInPreviewer(url);
|
||||
|
||||
const videoSection = document.querySelector('.video-section');
|
||||
if(videoSection){
|
||||
videoSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const page = '<?= $page ?>';
|
||||
|
||||
if (page === 'converter') {
|
||||
const streamUrlInput = document.getElementById('stream-url');
|
||||
|
||||
if (streamUrlInput && streamUrlInput.value) {
|
||||
playUrlInPreviewer(streamUrlInput.value);
|
||||
}
|
||||
|
||||
if(streamUrlInput) {
|
||||
streamUrlInput.addEventListener('input', () => {
|
||||
playUrlInPreviewer(streamUrlInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
if(saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const url = document.getElementById('stream-url').value;
|
||||
const filename = document.getElementById('filename').value;
|
||||
const token = document.getElementById('dropbox-token').value;
|
||||
|
||||
if (!url || !filename) {
|
||||
showToast('Preencha a URL e o nome do arquivo.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Salvando...';
|
||||
|
||||
try {
|
||||
const saveResponse = await fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'save', url, filename, token }),
|
||||
});
|
||||
const saveResult = await saveResponse.json();
|
||||
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
|
||||
|
||||
showToast(saveResult.message, 'success');
|
||||
setTimeout(() => location.href = '?page=converter', 1500);
|
||||
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '💾 Salvar e Converter';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Always update progress, regardless of page
|
||||
updateConversionProgress();
|
||||
setInterval(updateConversionProgress, 5000);
|
||||
});
|
||||
|
||||
async function updateConversionProgress() {
|
||||
const convertingRows = document.querySelectorAll('tr[data-status="converting"]');
|
||||
if (convertingRows.length === 0) return;
|
||||
|
||||
for (const row of convertingRows) {
|
||||
const streamId = row.dataset.streamId;
|
||||
if (!streamId) continue;
|
||||
|
||||
try {
|
||||
const response = await fetch(`api.php?action=get_conversion_progress&id=${streamId}`);
|
||||
if (!response.ok) continue;
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.progress !== undefined) {
|
||||
const progressBar = row.querySelector('.progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${result.progress}%`;
|
||||
}
|
||||
|
||||
if (result.progress >= 100) {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao atualizar progresso:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToDropbox(id, button) {
|
||||
const tokenInput = document.getElementById('dropbox-token');
|
||||
const token = tokenInput ? tokenInput.value : '';
|
||||
if (!token) {
|
||||
showToast('Por favor, insira seu token de acesso do Dropbox na página do conversor.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
button.innerHTML = 'Enviando...';
|
||||
showToast('Enviando arquivo para o Dropbox...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=send_to_dropbox', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, token }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Erro ao enviar para o Dropbox');
|
||||
|
||||
showToast(result.message, 'success');
|
||||
button.innerHTML = '✅ Enviado';
|
||||
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
button.disabled = false;
|
||||
button.innerHTML = '☁️';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVideo(id, button) {
|
||||
if (!confirm('Você tem certeza que deseja deletar este vídeo? Esta ação não pode ser desfeita.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
button.innerHTML = 'Apagando...';
|
||||
showToast('Apagando vídeo...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Erro ao apagar o vídeo');
|
||||
|
||||
showToast(result.message, 'success');
|
||||
button.closest('tr').remove();
|
||||
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
button.disabled = false;
|
||||
button.innerHTML = '🗑️';
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('show');
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
<script src="assets/js/app.js?v=<?= time() ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user