3
This commit is contained in:
parent
3ddc00e28e
commit
1496e8cc1d
20
.htaccess
20
.htaccess
@ -1,18 +1,2 @@
|
|||||||
DirectoryIndex index.php index.html
|
php_value upload_max_filesize 64M
|
||||||
Options -Indexes
|
php_value post_max_size 64M
|
||||||
Options -MultiViews
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# 0) Serve existing files/directories as-is
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -d
|
|
||||||
RewriteRule ^ - [L]
|
|
||||||
|
|
||||||
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
|
|
||||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
|
||||||
RewriteRule ^(.+?)/?$ $1.php [L]
|
|
||||||
|
|
||||||
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.+)/$ $1 [R=301,L]
|
|
||||||
@ -3,8 +3,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const sceneListEl = document.querySelector('.scene-list');
|
const sceneListEl = document.querySelector('.scene-list');
|
||||||
const previewPanel = document.getElementById('preview-panel');
|
const previewPanel = document.getElementById('preview-panel');
|
||||||
const programPanel = document.getElementById('program-panel');
|
const programPanel = document.getElementById('program-panel');
|
||||||
const previewContent = previewPanel.querySelector('.scene-content');
|
|
||||||
const programContent = programPanel.querySelector('.scene-content');
|
|
||||||
const cutButton = document.getElementById('cut-button');
|
const cutButton = document.getElementById('cut-button');
|
||||||
|
|
||||||
// Modal Elements
|
// Modal Elements
|
||||||
@ -19,14 +17,37 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
// Source settings elements
|
// Source settings elements
|
||||||
const colorSettings = document.getElementById('source-color-group');
|
const colorSettings = document.getElementById('source-color-group');
|
||||||
const imageSettings = document.getElementById('source-image-group');
|
const imageSettings = document.getElementById('source-image-group');
|
||||||
|
const videoSettings = document.getElementById('source-video-group');
|
||||||
|
const cameraSettings = document.getElementById('source-camera-group');
|
||||||
const sceneColorInput = document.getElementById('scene-color-input');
|
const sceneColorInput = document.getElementById('scene-color-input');
|
||||||
const sceneImageUrlInput = document.getElementById('scene-image-url-input');
|
const sceneImageFileInput = document.getElementById('scene-image-file-input');
|
||||||
|
const sceneVideoFileInput = document.getElementById('scene-video-file-input');
|
||||||
|
|
||||||
|
// Camera specific elements
|
||||||
|
const cameraPermissionPrompt = document.getElementById('camera-permission-prompt');
|
||||||
|
const grantCameraPermissionBtn = document.getElementById('grant-camera-permission-btn');
|
||||||
|
const cameraDeviceSelection = document.getElementById('camera-device-selection');
|
||||||
|
const sceneCameraDeviceInput = document.getElementById('scene-camera-device-input');
|
||||||
|
|
||||||
// Data Store
|
// Data Store
|
||||||
let scenes = [];
|
let scenes = [];
|
||||||
let activePreviewSceneId = null;
|
let activePreviewSceneId = null;
|
||||||
let activeProgramSceneId = null;
|
let activeProgramSceneId = null;
|
||||||
let sceneIdCounter = 0;
|
let sceneIdCounter = 0;
|
||||||
|
let activeStreams = {}; // To hold references to active MediaStream objects
|
||||||
|
|
||||||
|
// --- STREAM MANAGEMENT ---
|
||||||
|
function stopStream(panel) {
|
||||||
|
const panelId = panel.id;
|
||||||
|
if (activeStreams[panelId]) {
|
||||||
|
activeStreams[panelId].getTracks().forEach(track => track.stop());
|
||||||
|
delete activeStreams[panelId];
|
||||||
|
}
|
||||||
|
const videoEl = panel.querySelector('video');
|
||||||
|
if (videoEl) {
|
||||||
|
videoEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- DATA MANAGEMENT ---
|
// --- DATA MANAGEMENT ---
|
||||||
function addScene(name, type, value) {
|
function addScene(name, type, value) {
|
||||||
@ -46,6 +67,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (activePreviewSceneId === id) {
|
if (activePreviewSceneId === id) {
|
||||||
clearPreview();
|
clearPreview();
|
||||||
}
|
}
|
||||||
|
if (activeProgramSceneId === id) {
|
||||||
|
clearProgram();
|
||||||
|
}
|
||||||
renderSceneList();
|
renderSceneList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,16 +96,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreview(sceneId) {
|
async function updatePreview(sceneId) {
|
||||||
const scene = getScene(sceneId);
|
const scene = getScene(sceneId);
|
||||||
if (!scene) return;
|
if (!scene) return;
|
||||||
|
|
||||||
activePreviewSceneId = scene.id;
|
activePreviewSceneId = scene.id;
|
||||||
|
|
||||||
// Clear previous styles
|
stopStream(previewPanel);
|
||||||
|
previewPanel.innerHTML = '<span class="panel-label">PREVIEW</span><div class="scene-content"></div>';
|
||||||
previewPanel.style.backgroundColor = '#000';
|
previewPanel.style.backgroundColor = '#000';
|
||||||
previewPanel.style.backgroundImage = 'none';
|
previewPanel.style.backgroundImage = 'none';
|
||||||
previewContent.textContent = scene.name;
|
previewPanel.querySelector('.scene-content').textContent = scene.name;
|
||||||
|
|
||||||
switch (scene.type) {
|
switch (scene.type) {
|
||||||
case 'color':
|
case 'color':
|
||||||
@ -90,39 +115,132 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
case 'image':
|
case 'image':
|
||||||
previewPanel.style.backgroundImage = `url('${scene.value}')`;
|
previewPanel.style.backgroundImage = `url('${scene.value}')`;
|
||||||
break;
|
break;
|
||||||
|
case 'video':
|
||||||
|
case 'camera':
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = scene.type === 'video';
|
||||||
|
video.autoplay = true;
|
||||||
|
video.style.width = '100%';
|
||||||
|
video.style.height = '100%';
|
||||||
|
video.style.objectFit = 'cover';
|
||||||
|
|
||||||
|
if (scene.type === 'video') {
|
||||||
|
video.src = scene.value;
|
||||||
|
} else { // camera
|
||||||
|
try {
|
||||||
|
// A deviceId of '' can happen if the initial scene is a camera
|
||||||
|
// before permission is granted. We should ask for default camera.
|
||||||
|
const constraints = scene.value
|
||||||
|
? { video: { deviceId: { exact: scene.value } } }
|
||||||
|
: { video: true };
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
video.srcObject = stream;
|
||||||
|
activeStreams[previewPanel.id] = stream;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error accessing camera:", err);
|
||||||
|
previewPanel.querySelector('.scene-content').textContent = 'Camera Error!';
|
||||||
|
previewPanel.style.backgroundColor = 'red';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previewPanel.querySelector('.scene-content').appendChild(video);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
previewPanel.style.backgroundColor = '#000';
|
previewPanel.style.backgroundColor = '#000';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
renderSceneList(); // Re-render to show active state
|
renderSceneList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPreview() {
|
function clearPreview() {
|
||||||
|
stopStream(previewPanel);
|
||||||
activePreviewSceneId = null;
|
activePreviewSceneId = null;
|
||||||
previewContent.textContent = 'Select a Scene';
|
previewPanel.innerHTML = '<span class="panel-label">PREVIEW</span><div class="scene-content">Select a Scene</div>';
|
||||||
previewPanel.style.backgroundColor = '#000';
|
previewPanel.style.backgroundColor = '#000';
|
||||||
previewPanel.style.backgroundImage = 'none';
|
previewPanel.style.backgroundImage = 'none';
|
||||||
renderSceneList();
|
renderSceneList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearProgram() {
|
||||||
|
stopStream(programPanel);
|
||||||
|
activeProgramSceneId = null;
|
||||||
|
programPanel.innerHTML = '<span class="panel-label">PROGRAM (ON AIR)</span><div class="scene-content"></div>';
|
||||||
|
programPanel.style.backgroundColor = '#440000';
|
||||||
|
programPanel.style.backgroundImage = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
function getIconForSceneType(type) {
|
function getIconForSceneType(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'color': return '<i class="bi bi-palette-fill"></i>';
|
case 'color': return '<i class="bi bi-palette-fill"></i>';
|
||||||
case 'image': return '<i class="bi bi-image-fill"></i>';
|
case 'image': return '<i class="bi bi-image-fill"></i>';
|
||||||
case 'video': return '<i class="bi bi-film"></i>';
|
case 'video': return '<i class="bi bi-film"></i>';
|
||||||
|
case 'camera': return '<i class="bi bi-camera-video-fill"></i>';
|
||||||
default: return '<i class="bi bi-question-circle-fill"></i>';
|
default: return '<i class="bi bi-question-circle-fill"></i>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CAMERA & DEVICE MANAGEMENT ---
|
||||||
|
async function populateCameraDevices() {
|
||||||
|
// First, check for a secure context (HTTPS), which is required for getUserMedia.
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
console.error("Camera access requires a secure context (HTTPS).");
|
||||||
|
cameraPermissionPrompt.innerHTML = '<p class="text-danger">Camera access is only available over a secure HTTPS connection. Please ensure you are using an https:// URL.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Request permission first
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
|
||||||
|
// Stop the tracks immediately, we only needed it for permission
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
|
||||||
|
sceneCameraDeviceInput.innerHTML = '';
|
||||||
|
if (videoDevices.length === 0) {
|
||||||
|
// This case is unlikely if getUserMedia succeeded, but good to have.
|
||||||
|
cameraPermissionPrompt.innerHTML = '<p class="text-warning">No camera devices were found.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoDevices.forEach(device => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = device.deviceId;
|
||||||
|
option.textContent = device.label || `Camera ${sceneCameraDeviceInput.length + 1}`;
|
||||||
|
sceneCameraDeviceInput.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch UI
|
||||||
|
cameraPermissionPrompt.style.display = 'none';
|
||||||
|
cameraDeviceSelection.style.display = 'block';
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Could not get camera permissions or list devices:", err);
|
||||||
|
let errorMessage = 'An unexpected error occurred while trying to access the camera.';
|
||||||
|
|
||||||
|
if (err.name === 'NotAllowedError') {
|
||||||
|
errorMessage = 'Camera permission was denied. You need to grant permission in your browser\'s site settings to use this feature.';
|
||||||
|
} else if (err.name === 'NotFoundError') {
|
||||||
|
errorMessage = 'No camera was found on your device. Please connect a camera and try again.';
|
||||||
|
} else if (err.name === 'NotReadableError') {
|
||||||
|
errorMessage = 'The camera is currently in use by another application or a hardware error occurred.';
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraPermissionPrompt.innerHTML = `<p class="text-danger">${errorMessage}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- EVENT LISTENERS ---
|
// --- EVENT LISTENERS ---
|
||||||
|
|
||||||
// Scene list interactions (select, remove)
|
|
||||||
sceneListEl.addEventListener('click', (e) => {
|
sceneListEl.addEventListener('click', (e) => {
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
const sceneItem = target.closest('.list-group-item');
|
const sceneItem = target.closest('.list-group-item');
|
||||||
if (!sceneItem) return;
|
if (!sceneItem) return;
|
||||||
|
|
||||||
const sceneId = parseInt(sceneItem.dataset.sceneId);
|
const sceneId = parseInt(sceneItem.dataset.sceneId, 10);
|
||||||
|
|
||||||
if (target.classList.contains('remove-scene-btn')) {
|
if (target.classList.contains('remove-scene-btn')) {
|
||||||
removeScene(sceneId);
|
removeScene(sceneId);
|
||||||
@ -131,22 +249,41 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transition button
|
|
||||||
cutButton.addEventListener('click', () => {
|
cutButton.addEventListener('click', () => {
|
||||||
if (activePreviewSceneId === null) return;
|
if (activePreviewSceneId === null) return;
|
||||||
|
|
||||||
const previewScene = getScene(activePreviewSceneId);
|
const previewScene = getScene(activePreviewSceneId);
|
||||||
activeProgramSceneId = previewScene.id;
|
activeProgramSceneId = previewScene.id;
|
||||||
|
|
||||||
programContent.textContent = previewScene.name;
|
stopStream(programPanel);
|
||||||
|
|
||||||
|
// Clone the content and style
|
||||||
|
programPanel.innerHTML = previewPanel.innerHTML;
|
||||||
programPanel.style.backgroundColor = previewPanel.style.backgroundColor;
|
programPanel.style.backgroundColor = previewPanel.style.backgroundColor;
|
||||||
programPanel.style.backgroundImage = previewPanel.style.backgroundImage;
|
programPanel.style.backgroundImage = previewPanel.style.backgroundImage;
|
||||||
|
|
||||||
|
const previewVideo = previewPanel.querySelector('video');
|
||||||
|
if (previewVideo && previewVideo.srcObject) { // It's a stream
|
||||||
|
// Move the stream from preview to program
|
||||||
|
activeStreams[programPanel.id] = activeStreams[previewPanel.id];
|
||||||
|
delete activeStreams[previewPanel.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
programPanel.querySelector('.panel-label').textContent = 'PROGRAM (ON AIR)';
|
||||||
|
const programVideo = programPanel.querySelector('video');
|
||||||
|
if(programVideo) {
|
||||||
|
programVideo.muted = false; // Unmute in program
|
||||||
|
}
|
||||||
|
|
||||||
clearPreview();
|
clearPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modal interactions
|
function showModal() {
|
||||||
function showModal() { modal.style.display = 'flex'; sceneNameInput.focus(); }
|
modal.style.display = 'flex';
|
||||||
|
sceneNameInput.focus();
|
||||||
|
// Reset and update visibility on open
|
||||||
|
sceneTypeSelect.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
function hideModal() { modal.style.display = 'none'; }
|
function hideModal() { modal.style.display = 'none'; }
|
||||||
|
|
||||||
addSceneBtn.addEventListener('click', showModal);
|
addSceneBtn.addEventListener('click', showModal);
|
||||||
@ -157,9 +294,24 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const type = e.target.value;
|
const type = e.target.value;
|
||||||
colorSettings.style.display = type === 'color' ? 'block' : 'none';
|
colorSettings.style.display = type === 'color' ? 'block' : 'none';
|
||||||
imageSettings.style.display = type === 'image' ? 'block' : 'none';
|
imageSettings.style.display = type === 'image' ? 'block' : 'none';
|
||||||
|
videoSettings.style.display = type === 'video' ? 'block' : 'none';
|
||||||
|
cameraSettings.style.display = type === 'camera' ? 'block' : 'none';
|
||||||
|
|
||||||
|
if (type === 'camera') {
|
||||||
|
// Reset to initial state
|
||||||
|
cameraPermissionPrompt.style.display = 'block';
|
||||||
|
cameraDeviceSelection.style.display = 'none';
|
||||||
|
// Check if devices are already populated
|
||||||
|
if (sceneCameraDeviceInput.options.length > 0) {
|
||||||
|
cameraPermissionPrompt.style.display = 'none';
|
||||||
|
cameraDeviceSelection.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
modalSaveBtn.addEventListener('click', () => {
|
grantCameraPermissionBtn.addEventListener('click', populateCameraDevices);
|
||||||
|
|
||||||
|
async function saveScene() {
|
||||||
const name = sceneNameInput.value.trim();
|
const name = sceneNameInput.value.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
alert('Please enter a scene name.');
|
alert('Please enter a scene name.');
|
||||||
@ -167,49 +319,83 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
const type = sceneTypeSelect.value;
|
const type = sceneTypeSelect.value;
|
||||||
let value;
|
let value;
|
||||||
|
let file = null;
|
||||||
|
|
||||||
if (type === 'color') {
|
try {
|
||||||
value = sceneColorInput.value;
|
if (type === 'color') {
|
||||||
} else if (type === 'image') {
|
value = sceneColorInput.value;
|
||||||
value = sceneImageUrlInput.value.trim();
|
} else if (type === 'camera') {
|
||||||
if (!value) {
|
value = sceneCameraDeviceInput.value;
|
||||||
alert('Please enter an image URL.');
|
if (!value) {
|
||||||
return;
|
alert('Please select a camera device.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (type === 'image') {
|
||||||
|
file = sceneImageFileInput.files[0];
|
||||||
|
if (!file) { alert('Please select an image file.'); return; }
|
||||||
|
} else if (type === 'video') {
|
||||||
|
file = sceneVideoFileInput.files[0];
|
||||||
|
if (!file) { alert('Please select a video file.'); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
modalSaveBtn.disabled = true;
|
||||||
|
modalSaveBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Uploading...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch('upload.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
value = result.filePath;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Upload failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newScene = addScene(name, type, value);
|
||||||
|
updatePreview(newScene.id);
|
||||||
|
|
||||||
|
// Reset form and hide modal
|
||||||
|
sceneNameInput.value = '';
|
||||||
|
sceneColorInput.value = '#1e90ff';
|
||||||
|
sceneImageFileInput.value = '';
|
||||||
|
sceneVideoFileInput.value = '';
|
||||||
|
// Don't clear camera list, just reset selection
|
||||||
|
sceneTypeSelect.value = 'color';
|
||||||
|
colorSettings.style.display = 'block';
|
||||||
|
imageSettings.style.display = 'none';
|
||||||
|
videoSettings.style.display = 'none';
|
||||||
|
cameraSettings.style.display = 'none';
|
||||||
|
hideModal();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
modalSaveBtn.disabled = false;
|
||||||
|
modalSaveBtn.textContent = 'Save Scene';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newScene = addScene(name, type, value);
|
modalSaveBtn.addEventListener('click', saveScene);
|
||||||
updatePreview(newScene.id);
|
|
||||||
|
|
||||||
// Reset form and hide modal
|
|
||||||
sceneNameInput.value = '';
|
|
||||||
sceneColorInput.value = '#1e90ff';
|
|
||||||
sceneImageUrlInput.value = '';
|
|
||||||
sceneTypeSelect.value = 'color';
|
|
||||||
colorSettings.style.display = 'block';
|
|
||||||
imageSettings.style.display = 'none';
|
|
||||||
hideModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- INITIALIZATION ---
|
// --- INITIALIZATION ---
|
||||||
function initialize() {
|
function initialize() {
|
||||||
// Add some default scenes
|
// Set a default scene that doesn't require permissions on load
|
||||||
addScene('Main Camera', 'color', '#003366');
|
|
||||||
addScene('Starting Soon Screen', 'image', 'https://images.pexels.com/photos/1762851/pexels-photo-1762851.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1');
|
addScene('Starting Soon Screen', 'image', 'https://images.pexels.com/photos/1762851/pexels-photo-1762851.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1');
|
||||||
|
addScene('Main Camera', 'camera', ''); // Default camera, will ask for permission on use
|
||||||
|
addScene('Promo Video', 'video', 'https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4');
|
||||||
addScene('Screen Share', 'color', '#006633');
|
addScene('Screen Share', 'color', '#006633');
|
||||||
|
|
||||||
// Set initial program scene for display
|
const firstScene = getScene(0);
|
||||||
const firstScene = scenes[0];
|
|
||||||
if (firstScene) {
|
if (firstScene) {
|
||||||
activeProgramSceneId = firstScene.id;
|
updatePreview(firstScene.id);
|
||||||
programContent.textContent = firstScene.name;
|
|
||||||
programPanel.style.backgroundColor = firstScene.value;
|
|
||||||
programPanel.style.backgroundImage = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the second scene for preview initially
|
|
||||||
if (scenes.length > 1) {
|
|
||||||
updatePreview(scenes[1].id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
assets/uploads/images/img_690e7702877914.36415344.png
Normal file
BIN
assets/uploads/images/img_690e7702877914.36415344.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/uploads/videos/vid_690e77a9244553.28824953.mp4
Normal file
BIN
assets/uploads/videos/vid_690e77a9244553.28824953.mp4
Normal file
Binary file not shown.
24
index.php
24
index.php
@ -95,7 +95,8 @@
|
|||||||
<select class="form-select" id="scene-type-select">
|
<select class="form-select" id="scene-type-select">
|
||||||
<option value="color">Solid Color</option>
|
<option value="color">Solid Color</option>
|
||||||
<option value="image">Image</option>
|
<option value="image">Image</option>
|
||||||
<option value="video" disabled>Video (soon)</option>
|
<option value="video">Video</option>
|
||||||
|
<option value="camera">Camera</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -105,10 +106,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="source-image-group" class="mb-3 source-settings" style="display: none;">
|
<div id="source-image-group" class="mb-3 source-settings" style="display: none;">
|
||||||
<label for="scene-image-url-input" class="form-label">Image URL</label>
|
<label for="scene-image-file-input" class="form-label">Image File</label>
|
||||||
<input type="url" class="form-control" id="scene-image-url-input" placeholder="https://example.com/image.jpg">
|
<input type="file" class="form-control" id="scene-image-file-input" accept="image/*">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="source-video-group" class="mb-3 source-settings" style="display: none;">
|
||||||
|
<label for="scene-video-file-input" class="form-label">Video File</label>
|
||||||
|
<input type="file" class="form-control" id="scene-video-file-input" accept="video/*">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="source-camera-group" class="mb-3 source-settings" style="display: none;">
|
||||||
|
<div id="camera-permission-prompt" class="text-center">
|
||||||
|
<p>Please grant camera permission to proceed.</p>
|
||||||
|
<button type="button" id="grant-camera-permission-btn" class="btn btn-primary">Grant Permission</button>
|
||||||
|
</div>
|
||||||
|
<div id="camera-device-selection" style="display: none;">
|
||||||
|
<label for="scene-camera-device-input" class="form-label">Camera Device</label>
|
||||||
|
<select class="form-select" id="scene-camera-device-input"></select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Other source type options will go here -->
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
65
upload.php
Normal file
65
upload.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$response = ['success' => false, 'error' => 'An unknown error occurred.'];
|
||||||
|
|
||||||
|
if (isset($_FILES['file'])) {
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$response['error'] = 'File upload error: ' . $file['error'];
|
||||||
|
} else {
|
||||||
|
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
$fileMime = mime_content_type($file['tmp_name']);
|
||||||
|
|
||||||
|
$targetDir = '';
|
||||||
|
$allowedTypes = [];
|
||||||
|
$maxSize = 0;
|
||||||
|
$prefix = '';
|
||||||
|
|
||||||
|
// Determine settings based on file type
|
||||||
|
if (strpos($fileMime, 'image/') === 0) {
|
||||||
|
$targetDir = 'assets/uploads/images/';
|
||||||
|
$allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
$maxSize = 5 * 1024 * 1024; // 5 MB
|
||||||
|
$prefix = 'img_';
|
||||||
|
} elseif (strpos($fileMime, 'video/') === 0) {
|
||||||
|
$targetDir = 'assets/uploads/videos/';
|
||||||
|
$allowedTypes = ['mp4', 'webm', 'mov', 'ogv'];
|
||||||
|
$maxSize = 50 * 1024 * 1024; // 50 MB
|
||||||
|
$prefix = 'vid_';
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Unsupported file type: ' . $fileMime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($targetDir) {
|
||||||
|
if (!is_dir($targetDir)) {
|
||||||
|
mkdir($targetDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($fileExt, $allowedTypes)) {
|
||||||
|
$response['error'] = 'Invalid file extension. Allowed: ' . implode(', ', $allowedTypes);
|
||||||
|
} elseif ($file['size'] > $maxSize) {
|
||||||
|
$response['error'] = 'File is too large. Maximum size is ' . ($maxSize / 1024 / 1024) . ' MB.';
|
||||||
|
} else {
|
||||||
|
$fileName = preg_replace("/[^a-zA-Z0-9-_\.]/", "", basename($file['name']));
|
||||||
|
$uniqueName = $prefix . uniqid('', true) . '.' . $fileExt;
|
||||||
|
$targetPath = $targetDir . $uniqueName;
|
||||||
|
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'filePath' => $targetPath
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Failed to move uploaded file.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'No file uploaded.';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
?>
|
||||||
Loading…
x
Reference in New Issue
Block a user