<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Consentz Editor</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Noto+Serif:ital,wght@0,100..900;1,100..900&family=Oswald:wght@200..700&display=swap"
rel="stylesheet">
<!-- GrapesJS -->
<style>
body, html {
margin: 0;
overflow-x: hidden;
}
/* Dark mode input styles */
.bg-dark input,
.bg-dark select,
.bg-dark textarea {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #444;
}
.bg-dark input::placeholder,
.bg-dark textarea::placeholder {
color: #aaa;
}
.bg-dark .form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
}
.bg-dark .form-group,
.bg-dark .card,
.bg-dark .editor-section {
background-color: #2b2b2b;
border-color: #444;
}
.wrapper {
display: flex;
height: 100vh;
flex-direction: column;
}
.topbar {
height: 40px;
background-color: #000;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
}
.main {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 350px;
background-color: #000000ff;
padding: 20px;
border-right: 1px solid #000000ff;
color:white;
overflow-y:auto;
}
.content {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #fff;
text-align:-webkit-center;
}
.page-header {
background-color: #343a40;
color: #fff;
padding: 20px;
}
#gjs {
min-height: 70vh;
padding: 10px;
background: #fefefe;
}
.dropdown-box a {
display: block;
padding: 6px 12px;
color: #000;
text-decoration: none;
}
.dropdown-box a:hover {
background-color: #f1f1f1;
}
#preview-options {
position: absolute;
top: 80px;
right: 40px;
background: white;
border: 1px solid rgb(221, 221, 221);
padding: 5px;
z-index: 999;
display: none;
}
button{
background-color: #000000ff;
color:#f1f1f1;
border:none;
}
.edit-button {
position: absolute;
top: 8px;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
left: 38%;
width: fit-content;
}
.editable-image {
position: relative;
display: inline-block;
}
.fa-eye{
color:#f1f1f1 !important;
}
</style>
</head>
<body>
<div class="wrapper">
<!-- HEADER -->
<div class="topbar">
<div>
<strong>Consentz</strong>
</div>
<div>
<button type="button" onclick="toggleFullScreen()" title="Full Screen"><i class="fa fa-arrows-alt"></i></button>
<button type="button" title="Pages">Pages</button>
<button type="button" title="Preview"><a href="{{ path('temp_preview',{'clinic':clinic.id,'template':template.id}) }}" target="_blank"><i class="fa fa-eye"></i></a></button>
<button type="button" onclick="processVideo()" title="Save Template"><i class="fa fa-save"></i></button>
</div>
</div>
<!-- MAIN -->
<div class="main">
<!-- Sidebar -->
<!-- Content -->
<div class="content">
<div id="alerts-container"></div>
<div id="gjs">{{ template.body|raw }}</div>
<!-- Preview Dropdown -->
<div id="preview-options">
<div class="dropdown-link">
<div class="dropdown-content">
{% for key, allTemplate in allTemplates %}
{% if allTemplate.id != template.id %}
<div class="dropdown-box">
<a href="{{ path('temp_index', { 'clinic': clinic.id, 'template': allTemplate.id }) }}">
Page {{ key + 1 }}
</a>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
<div class="sidebar bg-dark text-white ">
<h4 class="text-white">Style Manager</h4>
<p>Select an element before using</p>
<form id="text-image-form" style="display:none;">
<div>
<button class="btn btn-primary" type="button" onclick="generateTextImage(this)">Submit</button>
</div>
<div class="accordion" id="styleAccordion">
<!-- 1. Text Content -->
<div class="accordion-item bg-dark text-white border-0">
<h2 class="accordion-header" id="headingText">
<button class="accordion-button bg-dark text-white collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseText" aria-expanded="true" aria-controls="#collapseText">
<i class="bi bi-type-bold me-2"></i>Text Content
</button>
</h2>
<div id="collapseText" class="accordion-collapse collapse show" >
<div class="accordion-body">
<div class="mb-3">
<textarea class="form-control form-control-lg" rows="4"
placeholder="Enter your text here..." required
style="resize: vertical; min-height: 100px;" id="modal-text">Hello</textarea>
<p>This text will appear as an overlay on your video</p>
</div>
</div>
</div>
</div>
<!-- 2. Typography -->
<div class="accordion-item bg-dark text-white border-0">
<h2 class="accordion-header" id="headingTypography">
<button class="accordion-button bg-dark text-white collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTypography" aria-expanded="true" aria-controls="#collapseTypography">
<i class="bi bi-fonts me-2"></i>Typography
</button>
</h2>
<div id="collapseTypography" class="accordion-collapse collapse" >
<div class="accordion-body">
{# Place your Typography section here #}
<div class="row mb-3">
<div class="col-md-6">
<label for="modal-fontType" class="form-label fw-semibold">Font Source</label>
<select class="form-select" id="modal-fontType" onchange="handleModalFontTypeChange(this)">
<option value="google">Google Fonts</option>
<option value="upload">Custom Font Upload</option>
</select>
</div>
<div class="col-md-6" id="modal-google-font-section">
<label for="modal-googleFont" class="form-label fw-semibold">Google Font</label>
<select class="form-select" id="modal-googleFont">
<option value="Roboto">Roboto (Default)</option>
<option value="Open Sans">Open Sans</option>
<option value="Lato">Lato</option>
<option value="Montserrat">Montserrat</option>
<option value="Oswald">Oswald</option>
<option value="Source Sans Pro">Source Sans Pro</option>
<option value="Raleway">Raleway</option>
<option value="Poppins">Poppins</option>
<option value="Nunito">Nunito</option>
<option value="Playfair Display">Playfair Display</option>
<option value="Inter">Inter</option>
<option value="Roboto Slab">Roboto Slab</option>
</select>
</div>
<div class="col-12 mt-3" id="modal-font-upload-section" style="display: none;">
<label for="modal-fontFile" class="form-label fw-semibold">Upload Font File</label>
<input type="file" class="form-control" id="modal-fontFile" accept=".ttf,.otf,.woff,.woff2">
<div class="form-text">Supported formats: TTF, OTF, WOFF, WOFF2</div>
</div>
</div>
<!-- Font Size & Colors -->
<div class="row mb-3">
<div class="col-md-6">
<label for="modal-fontSize" class="form-label fw-semibold">Font Size</label>
<div class="input-group">
<input type="range" class="form-range" id="modal-fontSize-range"
min="8" max="200" oninput="updateFontSizeInput(this.value)">
<input type="number" class="form-control form-control-sm" id="modal-fontSize"
min="8" max="200" style="max-width: 80px;"
oninput="updateFontSizeRange(this.value)">
<span class="input-group-text">px</span>
</div>
</div>
<div class="col-md-6">
<label for="modal-fontColor" class="form-label fw-semibold">Text Color</label>
<div class="input-group">
<input type="color" class="form-control form-control-color" id="modal-fontColor"
title="Choose text color">
<input type="text" class="form-control form-control-sm" id="modal-fontColor-text"
maxlength="7"
oninput="updateColorFromText('fontColor', this.value)">
</div>
</div>
<div class="col-md-6">
<label for="modal-backgroundColor" class="form-label fw-semibold">Background</label>
<div class="input-group">
<input type="color" class="form-control form-control-color" id="modal-backgroundColor"
title="Choose background color">
<input type="text" class="form-control form-control-sm" id="modal-backgroundColor-text"
placeholder="transparent"
oninput="updateColorFromText('backgroundColor', this.value)">
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="modal-transparentBg" checked>
<label class="form-check-label" for="modal-transparentBg">Transparent</label>
</div>
</div>
</div>
<!-- Text Spacing -->
<div class="row">
<div class="col-md-6">
<label for="modal-letterSpacing" class="form-label fw-semibold">Letter Spacing</label>
<div class="input-group">
<input type="range" class="form-range" id="modal-letterSpacing-range"
min="0" max="50" oninput="updateInputFromRange('letterSpacing', this.value)">
<input type="number" class="form-control form-control-sm" id="modal-letterSpacing"
min="0" max="50" style="max-width: 80px;">
<span class="input-group-text">px</span>
</div>
</div>
<div class="col-md-6">
<label for="modal-lineHeight" class="form-label fw-semibold">Line Height</label>
<div class="input-group">
<input type="range" class="form-range" id="modal-lineHeight-range"
min="0.5" max="3" step="0.1" oninput="updateInputFromRange('lineHeight', this.value)">
<input type="number" class="form-control form-control-sm" id="modal-lineHeight"
min="0.5" max="3" step="0.1" style="max-width: 80px;">
</div>
</div>
<div class="col-md-6">
<label for="modal-rotation" class="form-label fw-semibold">Rotation</label>
<div class="input-group">
<input type="range" class="form-range" id="modal-rotation-range"
min="-360" max="360" oninput="updateInputFromRange('rotation', this.value)">
<input type="number" class="form-control form-control-sm" id="modal-rotation"
min="-360" max="360" style="max-width: 80px;">
<span class="input-group-text">°</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 3. Layout & Positioning -->
<div class="accordion-item bg-dark text-white border-0">
<h2 class="accordion-header" id="headingLayout">
<button class="accordion-button bg-dark text-white collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLayout" aria-expanded="true" aria-controls="#collapseLayout">
<i class="bi bi-grid-3x3 me-2"></i>Layout & Positioning
</button>
</h2>
<div id="collapseLayout" class="accordion-collapse collapse">
<div class="accordion-body">
{# Place your Layout & Positioning section here #}
<div class="row mb-3">
<div class="col-md-12">
<label class="form-label fw-semibold">Text Alignment</label>
<div class="btn-group w-100" role="group" aria-label="Text alignment">
<input type="radio" class="btn-check" name="textAlign" id="align-left" value="left">
<label class="btn btn-outline-primary" for="align-left">
<i class="bi bi-text-left"></i> Left
</label>
<input type="radio" class="btn-check" name="textAlign" id="align-center" value="center" checked>
<label class="btn btn-outline-primary" for="align-center">
<i class="bi bi-text-center"></i> Center
</label>
<input type="radio" class="btn-check" name="textAlign" id="align-right" value="right">
<label class="btn btn-outline-primary" for="align-right">
<i class="bi bi-text-right"></i> Right
</label>
</div>
</div>
<div class="col-md-12 pt-2">
<label class="form-label fw-semibold">Vertical Position</label>
<div class="btn-group w-100" role="group" aria-label="Vertical alignment">
<input type="radio" class="btn-check" name="verticalAlign" id="valign-top" value="top">
<label class="btn btn-outline-secondary" for="valign-top">
<i class="bi bi-align-top"></i> Top
</label>
<input type="radio" class="btn-check" name="verticalAlign" id="valign-middle" value="middle" checked>
<label class="btn btn-outline-secondary" for="valign-middle">
<i class="bi bi-align-middle"></i> Middle
</label>
<input type="radio" class="btn-check" name="verticalAlign" id="valign-bottom" value="bottom">
<label class="btn btn-outline-secondary" for="valign-bottom">
<i class="bi bi-align-bottom"></i> Bottom
</label>
</div>
</div>
</div>
<!-- Dimensions -->
<div class="row mb-3">
<div class="col-md-6 pt-2">
<label for="modal-width" class="form-label fw-semibold">Width</label>
<div class="input-group">
<input type="number" class="form-control" id="modal-width" placeholder="Auto" min="1" >
<span class="input-group-text">px</span>
</div>
</div>
<div class="col-md-6 pt-2">
<label for="modal-height" class="form-label fw-semibold">Height</label>
<div class="input-group">
<input type="number" class="form-control" id="modal-height" placeholder="Auto" min="1">
<span class="input-group-text">px</span>
</div>
</div>
<div class="col-md-6 pt-2">
<label for="modal-padding" class="form-label fw-semibold">Padding</label>
<div class="input-group">
<input type="number" class="form-control" id="modal-padding" min="0">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<!-- Text Options -->
<div class="row">
<div class="col-md-12 pt-2">
<label for="modal-maxWidth" class="form-label fw-semibold">Max Width</label>
<div class="input-group">
<input type="number" class="form-control" id="modal-maxWidth" min="100">
<span class="input-group-text">px</span>
</div>
</div>
<div class="col-md-12 pt-2">
<label class="form-label fw-semibold">Text Options</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="modal-wordWrap">
<label class="form-check-label" for="modal-wordWrap">Word Wrap</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="orientation" id="modal-horizontal" value="horizontal" checked>
<label class="form-check-label" for="modal-horizontal">Horizontal</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="orientation" id="modal-vertical" value="vertical">
<label class="form-check-label" for="modal-vertical">Vertical</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 4. Effects & Styling -->
<div class="accordion-item bg-dark text-white border-0">
<h2 class="accordion-header" id="headingEffects">
<button class="accordion-button bg-dark text-white collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEffects" aria-expanded="true" aria-controls="#collapseEffects">
<i class="bi bi-magic me-2"></i>Effects & Styling
</button>
</h2>
<div id="collapseEffects" class="accordion-collapse collapse" >
<div class="accordion-body">
{# Place your Effects section here #}
<div class="row mb-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="modal-shadow">
<label class="form-check-label fw-semibold" for="modal-shadow">
<i class="bi bi-droplet-half me-1"></i>Text Shadow
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="modal-border">
<label class="form-check-label fw-semibold" for="modal-border">
<i class="bi bi-border-all me-1"></i>Text Border
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="modal-gradient">
<label class="form-check-label fw-semibold" for="modal-gradient">
<i class="bi bi-palette me-1"></i>Gradient
</label>
</div>
</div>
</div>
<!-- Shadow Settings -->
<div id="shadow-settings" style="display: none;">
<div class="alert alert-light border-start border-3 bg-dark text-white ">
<h6 class="alert-heading">Shadow Settings</h6>
<div class="row g-2">
<div class="col-md-6">
<label for="modal-shadowColor" class="form-label">Color</label>
<input type="color" class="form-control form-control-color" id="modal-shadowColor">
</div>
<div class="col-md-6">
<label for="modal-shadowOffsetX" class="form-label">Offset X</label>
<input type="number" class="form-control form-control-sm" id="modal-shadowOffsetX" >
</div>
<div class="col-md-6">
<label for="modal-shadowOffsetY" class="form-label">Offset Y</label>
<input type="number" class="form-control form-control-sm" id="modal-shadowOffsetY">
</div>
<div class="col-md-6">
<label for="modal-shadowBlur" class="form-label">Blur</label>
<input type="range" class="form-range" id="modal-shadowBlur" min="0" max="20" >
</div>
</div>
</div>
</div>
<!-- Border Settings -->
<div id="border-settings" style="display: none;">
<div class="alert alert-light border-start border-3 bg-dark text-white ">
<h6 class="alert-heading">Border Settings</h6>
<div class="row g-2">
<div class="col-md-6">
<label for="modal-borderColor" class="form-label">Color</label>
<input type="color" class="form-control form-control-color" id="modal-borderColor">
</div>
<div class="col-md-6">
<label for="modal-borderWidth" class="form-label">Width</label>
<input type="range" class="form-range" id="modal-borderWidth" min="1" max="20" >
</div>
</div>
</div>
</div>
<!-- Gradient Settings -->
<div id="gradient-settings" style="display: none;">
<div class="alert alert-light border-start border-3 bg-dark text-white ">
<h6 class="alert-heading">Gradient Settings</h6>
<div class="row g-2">
<div class="col-md-6">
<label for="modal-gradientFrom" class="form-label">From Color</label>
<input type="color" class="form-control form-control-color" id="modal-gradientFrom" >
</div>
<div class="col-md-6">
<label for="modal-gradientTo" class="form-label">To Color</label>
<input type="color" class="form-control form-control-color" id="modal-gradientTo" >
</div>
<div class="col-md-6">
<label for="modal-gradientDirection" class="form-label">Direction</label>
<select class="form-select form-select-sm" id="modal-gradientDirection">
<option value="horizontal">Horizontal</option>
<option value="vertical" selected>Vertical</option>
<option value="diagonal">Diagonal</option>
</select>
</div>
</div>
</div>
</div>
<!-- Opacity -->
<div class="mt-3">
<label for="modal-opacity" class="form-label fw-semibold">Overall Opacity</label>
<div class="d-flex align-items-center gap-3">
<input type="range" class="form-range flex-grow-1" id="modal-opacity"
min="0" max="1" step="0.1" value="1.0" oninput="updateOpacityDisplay(this.value)">
<span class="badge bg-secondary" id="opacity-display">100%</span>
</div>
</div>
</div>
</div>
</div>
<!-- 5. Video & Styling -->
<div class="accordion-item bg-dark text-white border-0" style="display:none;">
<h2 class="accordion-header" id="headingVideoData">
<button class="accordion-button bg-dark text-white collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseVideo" aria-expanded="true" aria-controls="#collapseVideo">
<i class="bi bi-grid-3x3 me-2"></i>Video Overlay Options
</button>
</h2>
<div id="collapseVideo" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="row g-4">
<div class="col-md-12">
<div class="card border-0 shadow-sm bg-dark text-white">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-clock me-2"></i>Timing Settings
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label for="modal-startTime" class="form-label fw-semibold">Start Time (seconds)</label>
<input type="number" class="form-control" id="modal-startTime" min="0" step="0.1" value="0">
<p>When to start showing this overlay</p>
</div>
<div class="mb-3">
<label for="modal-endTime" class="form-label fw-semibold">End Time (seconds)</label>
<input type="number" class="form-control" id="modal-endTime" min="0" step="0.1" value="5">
<p>When to stop showing this overlay (leave empty for entire video)</p>
</div>
<div class="mb-3">
<label for="modal-duration" class="form-label fw-semibold">Duration</label>
<input type="text" class="form-control" id="modal-duration" readonly>
<p>Calculated duration of overlay</p>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<div class="card border-0 shadow-sm bg-dark text-white">
<div class="card-header">
<h6 class="mb-0 text-primary">
<i class="bi bi-pin-map me-2"></i>Position Settings
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold">Position Preset</label>
<div class="row g-2">
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-top-left" value="top-left" checked>
<label class="btn btn-outline-primary btn-sm w-100" for="pos-top-left">↖ Top Left</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="modal-position" id="pos-top-center" value="top-center">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-top-center">↑ Top Center</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-top-right" value="top-right">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-top-right">↗ Top Right</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-center-left" value="center-left">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-center-left">← Left</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-center" value="center">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-center">⊙ Center</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-center-right" value="center-right">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-center-right">→ Right</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-bottom-left" value="bottom-left">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-bottom-left">↙ Bottom Left</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-bottom-center" value="bottom-center">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-bottom-center">↓ Bottom Center</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="position" id="pos-bottom-right" value="bottom-right">
<label class="btn btn-outline-primary btn-sm w-100" for="pos-bottom-right">↘ Bottom Right</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="video-customPosition" class="form-label fw-semibold">Custom Position (x:y)</label>
<input type="text" class="form-control" id="modal-customPosition" placeholder="e.g., 100:50">
<small class="text-muted">Override preset with custom coordinates</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-body text-center p-5">
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Loading...</span>
</div>
<h5 id="loading-text">Processing your request...</h5>
<p class="text-muted mb-0" id="loading-subtext">Please wait while we process your request</p>
</div>
</div>
</div>
</div>
<style>
body{
display:block !important;
}
</style>
<!-- Scripts -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<script>
function handleModalFontTypeChange(selectElement) {
const googleSection = document.getElementById('modal-google-font-section');
const uploadSection = document.getElementById('modal-font-upload-section');
const isGoogle = selectElement.value === 'google';
googleSection.style.display = isGoogle ? 'block' : 'none';
uploadSection.style.display = isGoogle ? 'none' : 'block';
}
function updateFontSizeInput(value) {
document.getElementById('modal-fontSize').value = value;
}
function updateFontSizeRange(value) {
document.getElementById('modal-fontSize-range').value = value;
}
function updateColorFromText(type, value) {
const colorInput = document.getElementById(`modal-${type}`);
if (/^#([0-9A-F]{3}){1,2}$/i.test(value)) {
colorInput.value = value;
}
}
function updateInputFromRange(id, value) {
const input = document.getElementById(`modal-${id}`);
input.value = value;
}
function updateOpacityDisplay(value) {
const percent = Math.round(value * 100);
document.getElementById('opacity-display').innerText = `${percent}%`;
}
// Show loading modal
function showLoading(title, subtitle) {
$('#loading-text').text(title);
$('#loading-subtext').text(subtitle);
$('#loadingModal').modal('show');
}
// Hide loading modal
function hideLoading() {
console.log('hideLoading() called');
// Multiple approaches to ensure modal is hidden
$('#loadingModal').modal('hide');
// Force hide with direct manipulation
$('#loadingModal').removeClass('show').addClass('fade');
$('#loadingModal').attr('aria-hidden', 'true');
$('#loadingModal').css('display', 'none');
// Force hide backdrop if it's stuck
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('overflow', '').css('padding-right', '');
// Extra safety timeout
setTimeout(function() {
$('#loadingModal').hide();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('overflow', '').css('padding-right', '');
}, 100);
}
// Show/hide settings
document.addEventListener('DOMContentLoaded', () => {
const toggles = [
{ id: 'modal-shadow', target: 'shadow-settings' },
{ id: 'modal-border', target: 'border-settings' },
{ id: 'modal-gradient', target: 'gradient-settings' }
];
toggles.forEach(toggle => {
const checkbox = document.getElementById(toggle.id);
const targetDiv = document.getElementById(toggle.target);
checkbox.addEventListener('change', () => {
targetDiv.style.display = checkbox.checked ? 'block' : 'none';
});
});
});
var templateData = {{ (template.data != '')?template.data|json_encode|raw:'[]' }};
if (templateData.startsWith("'") && templateData.endsWith("'")) {
templateData = templateData.slice(1, -1);
}
templateData = JSON.parse(templateData);
let defaltImage = false;
function getModalFormValues() {
const values = {
text: $('#modal-text').val(),
fontSize: $('#modal-fontSize').val(),
fontColor: $('#modal-fontColor-text').val(),
backgroundColor: $('#modal-transparentBg').is(':checked') ? 'transparent' : $('#modal-backgroundColor-text').val(),
textAlign: $('input[name="textAlign"]:checked').val(),
verticalAlign: $('input[name="verticalAlign"]:checked').val(),
orientation: $('input[name="orientation"]:checked').val(),
padding: $('#modal-padding').val(),
letterSpacing: $('#modal-letterSpacing').val(),
lineHeight: $('#modal-lineHeight').val(),
rotation: $('#modal-rotation').val(),
opacity: $('#modal-opacity').val(),
maxWidth: $('#modal-maxWidth').val(),
wordWrap: $('#modal-wordWrap').is(':checked'),
fontType: $('#modal-fontType').val(),
googleFont: $('#modal-googleFont').val(),
shadow: $('#modal-shadow').is(':checked'),
shadowColor: $('#modal-shadowColor').val(),
shadowOffsetX: $('#modal-shadowOffsetX').val(),
shadowOffsetY: $('#modal-shadowOffsetY').val(),
shadowBlur: $('#modal-shadowBlur').val(),
border: $('#modal-border').is(':checked'),
borderColor: $('#modal-borderColor').val(),
borderWidth: $('#modal-borderWidth').val(),
gradient: $('#modal-gradient').is(':checked'),
gradientFrom: $('#modal-gradientFrom').val(),
gradientTo: $('#modal-gradientTo').val(),
gradientDirection: $('#modal-gradientDirection').val(),
startTime:$('#modal-startTime').val(),
endTime:$('#modal-endTime').val(),
duration:videoDuration,
customPosition:$('#modal-customPosition').val(),
position:$('input[name="position"]:checked').val()
};
// Only include width and height if they have values
const width = $('#modal-width').val();
const height = $('#modal-height').val();
if (width !== null && width !== '' && width !== undefined) {
values.width = width;
}
if (height !== null && height !== '' && height !== undefined) {
values.height = height;
}
return values;
}
function generateTextImage(obj) {
$obj = $(obj);
const text = $('#modal-text').val().trim();
if (!text) {
showAlert('error', 'Please enter some text content.');
return;
}
$obj.attr('disabled',true);
$obj.html('Loading...');
const formValues = getModalFormValues();
console.log(formValues);
function sendJsonToApi(jsonData) {
$.ajax({
url: '/mytemplates/images/create',
type: 'POST',
data: JSON.stringify(jsonData),
processData: false,
contentType: 'application/json',
dataType: 'json',
timeout: 30000,
beforeSend: function () {
console.log('Sending image generation request...');
},
success: function (response) {
console.log('AJAX Success callback triggered');
console.log('API Response:', response);
templateData[defaltImage] = jsonData;
$obj.attr('disabled',false);
$obj.html('Submit');
try {
if (typeof response === 'string') {
response = JSON.parse(response);
}
const success = response.success || response.status === 1 || response.status === "1" || response.status === true;
const imageUrl = response.url || response.previewUrl || response.imagePath;
const message = response.message || 'Text image created successfully!';
if (success && imageUrl) {
$('#image-' + defaltImage).find('img').attr('src', imageUrl+ '?v=' + Date.now());
} else {
showAlert('error', 'Error: ' + (response.error || message || 'Failed to generate image'));
}
} catch (e) {
$obj.attr('disabled',false);
$obj.html('Submit');
console.error('Error processing response:', e, response);
showAlert('error', 'Error processing server response: ' + e.message);
}
},
error: function (xhr, status, error) {
$obj.attr('disabled',false);
$obj.html('Submit');
console.error('AJAX Error:', error);
if (status === 'timeout') {
showAlert('error', 'Request timed out. Please try again.');
} else {
showAlert('error', 'Network error: ' + error);
}
},
complete: function () {
console.log('AJAX Complete callback triggered');
$obj.attr('disabled',false);
$obj.html('Submit');
}
});
}
const img = $('#image-' + defaltImage).find('img');
const src = img.attr('src');
const cleanSrc = src.split('?')[0];
const jsonData = { ...formValues,"image":cleanSrc };
if (formValues.fontType === 'google') {
delete jsonData.fontData;
sendJsonToApi(jsonData);
} else {
const fileInput = document.getElementById('modal-fontFile');
if (fileInput && fileInput.files.length > 0) {
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function (e) {
const base64 = e.target.result.split(',')[1]; // Strip "data:...base64," prefix
jsonData.fontData = base64;
// Remove googleFont if exists
delete jsonData.googleFont;
sendJsonToApi(jsonData);
};
reader.readAsDataURL(file); // Converts file to base64
} else if (templateData[defaltImage] && templateData[defaltImage].fontData) {
// Use existing fontData from template
jsonData.fontData = templateData[defaltImage].fontData;
sendJsonToApi(jsonData);
}else {
showAlert('error', 'Please upload a font file.');
$obj.attr('disabled',false);
$obj.html('Submit');
}
}
}
function showAlert(type, message) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const iconClass = type === 'success' ? 'bi-check-circle' : 'bi-exclamation-triangle';
const alertHtml = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
<i class="${iconClass} me-2"></i>${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
$('#alerts-container').html(alertHtml);
// Auto-dismiss after 5 seconds
setTimeout(() => {
$('.alert').alert('close');
}, 5000);
}
let videoDuration = 0;
const video = document.getElementsByTagName('video')[0];
document.addEventListener('DOMContentLoaded', () => {
let videoDuration = 0;
if (!video) {
console.error('Video element not found');
return;
}
if (video.readyState >= 1) {
// Metadata is already loaded, handle accordingly
onMetadataLoaded();
} else {
video.addEventListener('loadedmetadata', onMetadataLoaded);
}
});
function onMetadataLoaded(){
videoDuration = parseInt(video.duration.toFixed()) + 1;
if (isNaN(videoDuration)) videoDuration = 0;
$('#video-endTime').val(videoDuration);
$('#video-duration').val(videoDuration);
// Wait until all .editable-image elements are rendered
const wrappers = document.querySelectorAll('.editable-image');
if (wrappers.length === 0) {
console.warn('No editable-image elements found');
return;
}
wrappers.forEach(wrapper => {
const button = document.createElement('button');
button.className = 'edit-button';
button.textContent = 'Edit';
button.style.display = 'none';
// Append button to wrapper first
wrapper.appendChild(button);
// Hover logic
wrapper.addEventListener('mouseenter', () => {
button.style.display = 'block';
});
wrapper.addEventListener('mouseleave', () => {
button.style.display = 'none';
});
// Click handler logic
button.addEventListener('click', (e) => {
e.stopPropagation();
const idPart = wrapper.getAttribute('id')?.replace('image-', '');
if (!templateData || !templateData[idPart]) {
console.warn('templateData not ready or missing for:', idPart);
return;
}
defaltImage = idPart;
$('#text-image-form').show();
Object.entries(templateData[idPart]).forEach(([key, value]) => {
$(`#modal-${key}, #modal-${key}-text, #modal-${key}-range`).val(value);
const $input = $(`input[name="${key}"]`);
if ($input.length) {
if ($input.attr('type') === 'radio') {
$(`input[name="${key}"][value="${value}"]`).prop('checked', true);
} else {
$input.val(value);
}
}
});
$('#modal-endTime, #modal-duration').val(videoDuration);
templateData[idPart].endTime = videoDuration;
// Position logic (customPosition)
if (!templateData[idPart].customPosition) {
const imageRect = wrapper.getBoundingClientRect();
const videoRect = video.getBoundingClientRect();
const relativeX = imageRect.left - videoRect.left;
const relativeY = imageRect.top - videoRect.top;
const mainWidth = video.videoWidth;
const mainHeight = video.videoHeight;
const percentWidth = relativeX !== 0 ? videoRect.width / relativeX : 0;
const percentHeight = relativeY !== 0 ? videoRect.height / relativeY : 0;
const renderTextRenderWidth = percentWidth !== 0 ? (mainWidth / percentWidth) : 0;
const renderTextRenderHeight = percentHeight !== 0 ? (mainHeight / percentHeight) : 0;
const pos = renderTextRenderWidth.toFixed() + ':' + renderTextRenderHeight.toFixed();
$('#modal-customPosition').val(pos);
templateData[idPart].customPosition = pos;
}
});
// Auto trigger first button click (optional – delay a bit)
setTimeout(() => button.click(), 100);
});
}
let isProcessing = false;
async function processVideo() {
if(templateData.length < 0){
return false;
}
if (isProcessing) return;
isProcessing = true;
showLoading('Generating video template...', 'Creating your custom text overlay with advanced styling');
try {
var videoTextParams = [];
templateData.forEach(function(value, index) {
const img = $('#image-' + index).find('img');
const src = img.attr('src');
const cleanSrc = src.split('?')[0];
const temp = {
imagePath: cleanSrc.replace('https://demo.tribital.com/test/video_editor/api/',''),
startTime: value.startTime || 0,
endTime: value.endTime || videoDuration,
position: (value.customPosition !== '') ? value.customPosition : (value.position || 'center'),
width: video.videoWidth*($('#image-'+index).width()/$('.background-video').width()),
height: video.videoHeight*($('#image-'+index).height()/$('.background-video').height()),
};
videoTextParams.push(temp);
});
const finalVideo = await processVideoWithMultipleOverlays(
videoTextParams
);
hideLoading();
showAlert('success', 'Template updated successfully');
isProcessing = false;
} catch (error) {
hideLoading();
isProcessing = false;
showAlert('error',`Error processing video`);
console.error('Error processing video:', error);
} finally {
isProcessing = false;
}
}
const mytemplate = {{template.id}};
const myclinic = {{template.clinic.id}};
async function processVideoWithMultipleOverlays(textImages) {
try {
const requestData = {
textImages: textImages,
mytemplate:mytemplate,
myclinic:myclinic,
templateData:templateData,
body:$('#video-container').html()
};
const response = await fetch('/admin/mytemplates/reels/edit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
hideLoading();
showAlert('error',`HTTP error! status: ${response.status}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
hideLoading();
showAlert('error',result.message || 'Failed to process video with multiple overlays');
throw new Error(result.message || 'Failed to process video with multiple overlays');
}
return result;
} catch (error) {
hideLoading();
showAlert('error',`Multiple overlay processing failed: ${error.message}`);
throw new Error(`Multiple overlay processing failed: ${error.message}`);
}
}
function toggleFullScreen() {
if (!document.fullscreenElement && // Check if not in fullscreen
!document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
// If not in fullscreen, request fullscreen
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) { // Firefox
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) { // Chrome, Safari, Opera
document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.msRequestFullscreen) { // IE/Edge
document.documentElement.msRequestFullscreen();
}
fullscreenBtn.textContent = "Exit Full Screen"; // Change button text
} else {
// If in fullscreen, exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { // Firefox
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { // Chrome, Safari, Opera
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { // IE/Edge
document.msExitFullscreen();
}
fullscreenBtn.textContent = "Go Full Screen"; // Change button text back
}
}
</script>