Features: - Add ENABLE_UI and ENABLE_API environment variables for deployment flexibility - Add API URL display textbox with copy button in web interface - Create /api/calculate-auto-values endpoint to centralize sizing logic - Dimension overlay now calls API for accurate calculations instead of duplicating logic Improvements: - Enhance auto-sizing algorithm with aspect-ratio awareness - Improve padding calculation using geometric mean formula - Font detection now uses all available fonts from get_available_fonts() - API URL includes all parameters (font_path, font_size, padding, etc.) UI/UX: - Remove hidden header section and logo copy buttons - API URL textarea wraps and grows vertically to show full URL - Clean up unused code and debug console.log statements Dependencies: - Add gunicorn==21.2.0 to requirements.txt Documentation: - Document environment variables with Docker and Python examples - Add production deployment guidance - Update API documentation with font_path parameter - Add API URL feature to usage instructions Bug fixes: - Change debug=False for production - Remove unused 're' import from app.py
1947 lines
77 KiB
HTML
1947 lines
77 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>logo-txt</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: #0f0f0f;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1800px;
|
|
margin: 0 auto;
|
|
background: #1a1a1a;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
overflow: hidden;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.main-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 0;
|
|
}
|
|
|
|
@media (min-width: 1200px) {
|
|
body {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.container {
|
|
border-radius: 0 !important;
|
|
border: none !important;
|
|
}
|
|
|
|
.main-layout {
|
|
grid-template-columns: 500px 1fr;
|
|
}
|
|
|
|
.form-container {
|
|
border-right: 1px solid #333;
|
|
max-height: 100vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.preview-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
padding: 40px;
|
|
background: #0f0f0f;
|
|
}
|
|
|
|
.preview-section {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.preview-section br {
|
|
display: none;
|
|
}
|
|
|
|
.logo-grid {
|
|
grid-template-columns: repeat(4, 1fr) !important;
|
|
grid-template-rows: repeat(2, 110px) !important;
|
|
grid-auto-rows: 110px !important;
|
|
max-height: 215px !important;
|
|
gap: 8px !important;
|
|
padding: 8px !important;
|
|
overflow-y: auto !important;
|
|
}
|
|
|
|
.logo-item {
|
|
height: 110px !important;
|
|
}
|
|
|
|
.logo-item-name {
|
|
font-size: 9px !important;
|
|
height: 22px !important;
|
|
}
|
|
|
|
.logo-item img {
|
|
height: calc(100% - 22px) !important;
|
|
padding: 6px !important;
|
|
}
|
|
|
|
/* Compact form controls for two-column layout */
|
|
.content {
|
|
padding: 8px !important;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 6px !important;
|
|
}
|
|
|
|
label {
|
|
margin-bottom: 2px !important;
|
|
font-size: 0.75em !important;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="number"],
|
|
select {
|
|
padding: 5px 7px !important;
|
|
font-size: 12px !important;
|
|
}
|
|
|
|
.file-input-label {
|
|
padding: 6px !important;
|
|
font-size: 12px !important;
|
|
}
|
|
|
|
.position-option label {
|
|
padding: 6px !important;
|
|
font-size: 12px !important;
|
|
}
|
|
|
|
input[type="color"] {
|
|
width: 40px !important;
|
|
height: 32px !important;
|
|
}
|
|
|
|
.tab-btn {
|
|
padding: 5px 8px !important;
|
|
font-size: 11px !important;
|
|
}
|
|
|
|
.preview-section {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: none !important;
|
|
}
|
|
|
|
.preview-section h2 {
|
|
margin-bottom: 10px;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.search-controls {
|
|
gap: 6px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.two-column {
|
|
gap: 6px;
|
|
}
|
|
|
|
small {
|
|
font-size: 0.7em;
|
|
}
|
|
}
|
|
|
|
.content {
|
|
padding: 40px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
color: #e0e0e0;
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="number"],
|
|
select {
|
|
width: 100%;
|
|
padding: 12px 15px;
|
|
border: 2px solid #333;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
transition: border-color 0.3s;
|
|
background: #2a2a2a;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
input[type="text"]:focus,
|
|
input[type="number"]:focus,
|
|
select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.file-input-wrapper {
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: inline-block;
|
|
width: 100%;
|
|
}
|
|
|
|
.file-input-wrapper input[type="file"] {
|
|
position: absolute;
|
|
left: -9999px;
|
|
}
|
|
|
|
.file-input-label {
|
|
display: block;
|
|
padding: 15px;
|
|
background: #2a2a2a;
|
|
border: 2px dashed #667eea;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.file-input-label:hover {
|
|
background: #333;
|
|
border-color: #764ba2;
|
|
}
|
|
|
|
.file-input-label.has-file {
|
|
background: #1e3a1e;
|
|
border-color: #4caf50;
|
|
color: #b3ffb3;
|
|
}
|
|
|
|
.color-input-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.color-input-group input[type="text"] {
|
|
flex: 1;
|
|
}
|
|
|
|
input[type="color"] {
|
|
width: 60px;
|
|
height: 45px;
|
|
border: 2px solid #333;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
background: #2a2a2a;
|
|
}
|
|
|
|
.position-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 10px;
|
|
}
|
|
|
|
.position-option {
|
|
position: relative;
|
|
}
|
|
|
|
.position-option input[type="radio"] {
|
|
position: absolute;
|
|
opacity: 0;
|
|
}
|
|
|
|
.position-option label {
|
|
display: block;
|
|
padding: 15px;
|
|
background: #2a2a2a;
|
|
border: 2px solid #333;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
margin: 0;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.position-option input[type="radio"]:checked + label {
|
|
background: #667eea;
|
|
color: white;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.position-option label:hover {
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.btn {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 15px 30px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.preview-section {
|
|
margin-top: 30px;
|
|
padding-top: 30px;
|
|
border-top: 2px solid #333;
|
|
text-align: center;
|
|
}
|
|
|
|
.preview-section h2 {
|
|
margin-bottom: 20px;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.preview-image {
|
|
max-width: 100%;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
background-image:
|
|
linear-gradient(45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(-45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(45deg, transparent 75%, #808080 75%),
|
|
linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
background-size: 20px 20px;
|
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
|
background-color: #999999;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.2s, opacity 0.3s;
|
|
}
|
|
|
|
.preview-image:hover {
|
|
transform: scale(1.02);
|
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.preview-image.loading {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.preview-loading-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(15, 15, 15, 0.7);
|
|
border-radius: 8px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.preview-loading-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.spinner {
|
|
width: 50px;
|
|
height: 50px;
|
|
border: 4px solid rgba(102, 126, 234, 0.2);
|
|
border-top-color: #667eea;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.preview-download-hint {
|
|
display: inline-block;
|
|
margin-top: 10px;
|
|
padding: 8px 16px;
|
|
background: rgba(102, 126, 234, 0.1);
|
|
border: 1px solid rgba(102, 126, 234, 0.3);
|
|
border-radius: 6px;
|
|
color: #667eea;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.preview-download-hint::before {
|
|
content: '⬇️ ';
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #667eea;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.error {
|
|
background: #ffebee;
|
|
color: #c62828;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
border-left: 4px solid #c62828;
|
|
}
|
|
|
|
.success {
|
|
background: #e8f5e9;
|
|
color: #2e7d32;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
border-left: 4px solid #2e7d32;
|
|
}
|
|
|
|
.download-btn {
|
|
display: inline-block;
|
|
margin-top: 15px;
|
|
padding: 12px 25px;
|
|
background: #4caf50;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.download-btn:hover {
|
|
background: #45a049;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
|
|
}
|
|
|
|
.advanced-options {
|
|
margin-top: 20px;
|
|
padding: 20px;
|
|
background: #2a2a2a;
|
|
border-radius: 8px;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.advanced-options h3 {
|
|
margin-bottom: 15px;
|
|
color: #e0e0e0;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.two-column {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 15px;
|
|
}
|
|
|
|
.input-tabs {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.tab-btn {
|
|
flex: 1;
|
|
padding: 10px 20px;
|
|
background: #2a2a2a;
|
|
border: 2px solid #333;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
color: #999;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab-btn:hover {
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.tab-btn.active {
|
|
background: #667eea;
|
|
color: white;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Custom Font Selector Styles */
|
|
.custom-select-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.custom-select {
|
|
width: 100%;
|
|
padding: 12px 40px 12px 15px;
|
|
border: 2px solid #333;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
background: #2a2a2a;
|
|
color: #e0e0e0;
|
|
cursor: pointer;
|
|
transition: border-color 0.3s;
|
|
user-select: none;
|
|
position: relative;
|
|
}
|
|
|
|
.custom-select:hover {
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.custom-select.open {
|
|
border-color: #667eea;
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
|
|
.custom-select::after {
|
|
content: '▼';
|
|
position: absolute;
|
|
right: 15px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 12px;
|
|
color: #666;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.custom-select.open::after {
|
|
transform: translateY(-50%) rotate(180deg);
|
|
}
|
|
|
|
.custom-options {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: #2a2a2a;
|
|
border: 2px solid #667eea;
|
|
border-top: none;
|
|
border-radius: 0 0 8px 8px;
|
|
z-index: 1000;
|
|
display: none;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.custom-options.open {
|
|
display: block;
|
|
}
|
|
|
|
.custom-option {
|
|
padding: 12px 15px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
font-size: 16px;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.custom-option:hover {
|
|
background: #333;
|
|
}
|
|
|
|
.custom-option.selected {
|
|
background: #667eea;
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.custom-option:first-child {
|
|
font-family: inherit;
|
|
}
|
|
|
|
/* Logo Browser Styles */
|
|
.search-controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.search-controls input {
|
|
flex: 2;
|
|
}
|
|
|
|
.search-controls select {
|
|
flex: 1;
|
|
}
|
|
|
|
.logo-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: 10px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
background: #2a2a2a;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.logo-item {
|
|
aspect-ratio: 3/4;
|
|
background:
|
|
linear-gradient(45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(-45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(45deg, transparent 75%, #808080 75%),
|
|
linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
background-size: 20px 20px;
|
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
|
background-color: #999999;
|
|
border: 2px solid #444;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.logo-item:hover {
|
|
border-color: #667eea;
|
|
transform: scale(1.05);
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
|
|
}
|
|
|
|
.logo-item.selected {
|
|
border-color: #4caf50;
|
|
background-color: #558855;
|
|
}
|
|
|
|
.logo-item img {
|
|
width: 100%;
|
|
height: calc(100% - 28px);
|
|
object-fit: contain;
|
|
padding: 8px;
|
|
}
|
|
|
|
.logo-item-name {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 28px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(26, 26, 26, 0.95);
|
|
border-top: 1px solid #444;
|
|
font-size: 10px;
|
|
color: #e0e0e0;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
padding: 0 6px;
|
|
}
|
|
|
|
.loading-spinner {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #667eea;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.two-column, .position-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 1.8em;
|
|
}
|
|
|
|
.content {
|
|
padding: 20px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="main-layout">
|
|
<div class="form-container">
|
|
<div class="content">
|
|
<div id="message"></div>
|
|
|
|
<form id="logoForm">
|
|
<div class="form-group">
|
|
<label>Logo Image</label>
|
|
<div class="input-tabs">
|
|
<button type="button" class="tab-btn active" data-tab="file">📁 Upload File</button>
|
|
<button type="button" class="tab-btn" data-tab="url">🔗 Image URL</button>
|
|
<button type="button" class="tab-btn" data-tab="browse">📺 Browse Logos</button>
|
|
</div>
|
|
<div id="fileTab" class="tab-content active">
|
|
<div class="file-input-wrapper">
|
|
<input type="file" id="imageFile" accept="image/*">
|
|
<label for="imageFile" class="file-input-label" id="fileLabel">
|
|
<span>📁 Choose an image file (PNG, JPG, GIF, WEBP)</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div id="urlTab" class="tab-content">
|
|
<input type="text" id="imageUrl" placeholder="https://example.com/logo.png">
|
|
</div>
|
|
<div id="browseTab" class="tab-content">
|
|
<div class="search-controls">
|
|
<input type="text" id="logoSearch" placeholder="🔍 Search by filename...">
|
|
<select id="countryFilter">
|
|
<option value="">All Countries</option>
|
|
</select>
|
|
</div>
|
|
<div id="logoGrid" class="logo-grid">
|
|
<div class="loading-spinner">Loading logos...</div>
|
|
</div>
|
|
<input type="hidden" id="selectedLogoUrl">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text">Text to Add</label>
|
|
<input type="text" id="text" placeholder="Enter your text here..." required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Text Position</label>
|
|
<div class="position-grid">
|
|
<div class="position-option">
|
|
<input type="radio" id="above" name="position" value="above">
|
|
<label for="above">⬆️ Above</label>
|
|
</div>
|
|
<div class="position-option">
|
|
<input type="radio" id="below" name="position" value="below" checked>
|
|
<label for="below">⬇️ Below</label>
|
|
</div>
|
|
<div class="position-option">
|
|
<input type="radio" id="left" name="position" value="left">
|
|
<label for="left">⬅️ Left</label>
|
|
</div>
|
|
<div class="position-option">
|
|
<input type="radio" id="right" name="position" value="right">
|
|
<label for="right">➡️ Right</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="advanced-options">
|
|
<h3>⚙️ Advanced Options</h3>
|
|
|
|
<div class="form-group">
|
|
<label for="fontSelect">Font</label>
|
|
<div class="custom-select-wrapper">
|
|
<div class="custom-select" id="customFontSelect">
|
|
<span id="selectedFontText">Auto (Best Match)</span>
|
|
</div>
|
|
<div class="custom-options" id="customFontOptions">
|
|
<!-- Options will be populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
<input type="hidden" id="fontSelect" value="auto">
|
|
<small id="usedFontDisplay" style="color: #667eea; font-size: 0.85em; display: none; margin-top: 4px;"></small>
|
|
</div>
|
|
|
|
<div class="two-column">
|
|
<div class="form-group">
|
|
<label for="fontSize">Font Size (pixels)</label>
|
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
<input type="number" id="fontSize" value="80" min="10" max="500" step="5" style="flex: 1;">
|
|
<label style="display: flex; align-items: center; gap: 6px; margin: 0; cursor: pointer; white-space: nowrap; font-weight: normal; color: #999;">
|
|
<input type="checkbox" id="fontSizeAuto" checked style="cursor: pointer;">
|
|
Auto
|
|
</label>
|
|
</div>
|
|
<input type="range" id="fontSizeSlider" min="10" max="500" step="5" value="80" style="width: 100%; margin-top: 8px; cursor: pointer;" disabled>
|
|
<small id="usedFontSizeDisplay" style="color: #667eea; font-size: 0.85em; display: none; margin-top: 4px;"></small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="padding">Padding (pixels)</label>
|
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
<input type="number" id="padding" value="20" min="0" max="200" step="5" style="flex: 1;">
|
|
<label style="display: flex; align-items: center; gap: 6px; margin: 0; cursor: pointer; white-space: nowrap; font-weight: normal; color: #999;">
|
|
<input type="checkbox" id="paddingAuto" checked style="cursor: pointer;">
|
|
Auto
|
|
</label>
|
|
</div>
|
|
<input type="range" id="paddingSlider" min="0" max="200" step="5" value="20" style="width: 100%; margin-top: 8px; cursor: pointer;" disabled>
|
|
<small id="usedPaddingDisplay" style="color: #667eea; font-size: 0.85em; display: none; margin-top: 4px;"></small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="textColor">Text Color</label>
|
|
<div class="color-input-group">
|
|
<input type="text" id="textColor" value="white" placeholder="white or #ffffff">
|
|
<input type="color" id="textColorPicker" value="#ffffff">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="bgColor">Background Color</label>
|
|
<div class="color-input-group">
|
|
<input type="text" id="bgColor" value="transparent" placeholder="transparent or #1a1a1a">
|
|
<input type="color" id="bgColorPicker" value="#1a1a1a">
|
|
</div>
|
|
<small style="color: #666; font-size: 0.85em;">Use "transparent" to preserve logo transparency</small>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn" id="submitBtn" style="display: none;">
|
|
✨ Generate Logo
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="preview-container">
|
|
<div id="preview" class="preview-section" style="display: none; width: 100%;">
|
|
<h2>Preview</h2>
|
|
<div style="padding: 30px;">
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="previewImage" class="preview-image" alt="Processed logo">
|
|
<div id="previewLoadingOverlay" class="preview-loading-overlay">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<div id="dimensionOverlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; display: none;">
|
|
<!-- Font size indicator -->
|
|
<div id="fontSizeIndicator" style="position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: none;">
|
|
<div id="fontSizeLabel" style="position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;"></div>
|
|
</div>
|
|
<!-- Padding indicator -->
|
|
<div id="paddingIndicator" style="position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: none;">
|
|
<div id="paddingLabel" style="position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<label style="display: flex; align-items: center; gap: 8px; margin: 10px auto; cursor: pointer; color: #e0e0e0; font-weight: normal; font-size: 14px; justify-content: center; max-width: 250px;">
|
|
<input type="checkbox" id="showDimensions" checked style="cursor: pointer;">
|
|
Show dimensions on preview
|
|
</label>
|
|
<div class="preview-download-hint">Click image to download</div>
|
|
<div style="margin-top: 20px; max-width: 800px; margin-left: auto; margin-right: auto;">
|
|
<label for="apiUrl" style="display: block; margin-bottom: 8px; color: #e0e0e0; font-weight: 600; font-size: 0.95em;">API Call URL:</label>
|
|
<textarea id="apiUrl" readonly style="width: 100%; padding: 12px 15px; border: 2px solid #333; border-radius: 8px; font-size: 14px; background: #2a2a2a; color: #e0e0e0; font-family: monospace; cursor: text; user-select: all; resize: vertical; min-height: 44px; line-height: 1.5; overflow-y: auto; white-space: pre-wrap; word-break: break-all;" onclick="this.select()"></textarea>
|
|
<small style="display: block; margin-top: 6px; color: #666; font-size: 0.85em;">Use this URL to programmatically generate the same logo via the API</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('logoForm');
|
|
const imageFile = document.getElementById('imageFile');
|
|
const imageUrl = document.getElementById('imageUrl');
|
|
const fileLabel = document.getElementById('fileLabel');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const messageDiv = document.getElementById('message');
|
|
const preview = document.getElementById('preview');
|
|
const previewImage = document.getElementById('previewImage');
|
|
const previewLoadingOverlay = document.getElementById('previewLoadingOverlay');
|
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
const fileTab = document.getElementById('fileTab');
|
|
const urlTab = document.getElementById('urlTab');
|
|
const browseTab = document.getElementById('browseTab');
|
|
const logoSearch = document.getElementById('logoSearch');
|
|
const countryFilter = document.getElementById('countryFilter');
|
|
const logoGrid = document.getElementById('logoGrid');
|
|
const selectedLogoUrl = document.getElementById('selectedLogoUrl');
|
|
const fontSelect = document.getElementById('fontSelect');
|
|
const customFontSelect = document.getElementById('customFontSelect');
|
|
const customFontOptions = document.getElementById('customFontOptions');
|
|
const selectedFontText = document.getElementById('selectedFontText');
|
|
const showDimensions = document.getElementById('showDimensions');
|
|
const dimensionOverlay = document.getElementById('dimensionOverlay');
|
|
const fontSizeIndicator = document.getElementById('fontSizeIndicator');
|
|
const paddingIndicator = document.getElementById('paddingIndicator');
|
|
const fontSizeLabel = document.getElementById('fontSizeLabel');
|
|
const paddingLabel = document.getElementById('paddingLabel');
|
|
let activeInputMode = 'file';
|
|
let availableFonts = [];
|
|
let allLogos = [];
|
|
let allCountries = [];
|
|
let displayedCount = 50;
|
|
let currentFilteredLogos = [];
|
|
let isLoadingMore = false;
|
|
|
|
// Custom select functionality
|
|
customFontSelect.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
customFontSelect.classList.toggle('open');
|
|
customFontOptions.classList.toggle('open');
|
|
});
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', () => {
|
|
customFontSelect.classList.remove('open');
|
|
customFontOptions.classList.remove('open');
|
|
});
|
|
|
|
function selectFont(value, text, fontFamily) {
|
|
fontSelect.value = value;
|
|
selectedFontText.textContent = text;
|
|
if (fontFamily) {
|
|
selectedFontText.style.fontFamily = fontFamily;
|
|
} else {
|
|
selectedFontText.style.fontFamily = 'inherit';
|
|
}
|
|
|
|
// Update selected state in options
|
|
document.querySelectorAll('.custom-option').forEach(opt => {
|
|
opt.classList.remove('selected');
|
|
});
|
|
event.target.classList.add('selected');
|
|
|
|
customFontSelect.classList.remove('open');
|
|
customFontOptions.classList.remove('open');
|
|
|
|
// Trigger live preview when font changes
|
|
triggerLivePreview();
|
|
// Update API URL when font changes
|
|
updateApiUrl();
|
|
}
|
|
|
|
// Load available fonts
|
|
async function loadFonts() {
|
|
try {
|
|
const response = await fetch('/api/fonts');
|
|
const data = await response.json();
|
|
availableFonts = data.fonts;
|
|
|
|
// Clear existing options
|
|
customFontOptions.innerHTML = '';
|
|
|
|
// Add "Auto" option
|
|
const autoOption = document.createElement('div');
|
|
autoOption.className = 'custom-option selected';
|
|
autoOption.textContent = 'Auto (Best Match)';
|
|
autoOption.addEventListener('click', () => {
|
|
selectFont('auto', 'Auto (Best Match)', null);
|
|
});
|
|
customFontOptions.appendChild(autoOption);
|
|
|
|
// Populate font options with font previews
|
|
availableFonts.forEach(font => {
|
|
const option = document.createElement('div');
|
|
option.className = 'custom-option';
|
|
option.textContent = font.name;
|
|
option.dataset.value = font.path;
|
|
|
|
// Apply the font's style to the option
|
|
const fontFamily = getFontFamilyForPreview(font.name);
|
|
option.style.fontFamily = fontFamily;
|
|
|
|
option.addEventListener('click', () => {
|
|
selectFont(font.path, font.name, fontFamily);
|
|
});
|
|
|
|
customFontOptions.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load fonts:', error);
|
|
}
|
|
}
|
|
|
|
// Map font names to CSS font families for preview
|
|
function getFontFamilyForPreview(fontName) {
|
|
const lowerName = fontName.toLowerCase();
|
|
|
|
// Google Fonts
|
|
if (lowerName.includes('roboto')) return 'Roboto, sans-serif';
|
|
if (lowerName.includes('open sans')) return 'Open Sans, sans-serif';
|
|
if (lowerName.includes('montserrat')) return 'Montserrat, sans-serif';
|
|
if (lowerName.includes('oswald')) return 'Oswald, sans-serif';
|
|
if (lowerName.includes('raleway')) return 'Raleway, sans-serif';
|
|
if (lowerName.includes('lato')) return 'Lato, sans-serif';
|
|
if (lowerName.includes('poppins')) return 'Poppins, sans-serif';
|
|
if (lowerName.includes('anton')) return 'Anton, sans-serif';
|
|
if (lowerName.includes('bebas neue')) return 'Bebas Neue, sans-serif';
|
|
if (lowerName.includes('pt sans')) return 'PT Sans, sans-serif';
|
|
if (lowerName.includes('nunito')) return 'Nunito, sans-serif';
|
|
|
|
// DejaVu fonts
|
|
if (lowerName.includes('dejavu sans mono')) return 'DejaVu Sans Mono, monospace';
|
|
if (lowerName.includes('dejavu sans')) return 'DejaVu Sans, sans-serif';
|
|
if (lowerName.includes('dejavu serif')) return 'DejaVu Serif, serif';
|
|
|
|
// Liberation fonts
|
|
if (lowerName.includes('liberation sans')) return 'Liberation Sans, sans-serif';
|
|
if (lowerName.includes('liberation serif')) return 'Liberation Serif, serif';
|
|
if (lowerName.includes('liberation mono')) return 'Liberation Mono, monospace';
|
|
|
|
// Noto fonts
|
|
if (lowerName.includes('noto sans')) return 'Noto Sans, sans-serif';
|
|
if (lowerName.includes('noto serif')) return 'Noto Serif, serif';
|
|
if (lowerName.includes('noto mono')) return 'Noto Mono, monospace';
|
|
|
|
// FreeFonts
|
|
if (lowerName.includes('freesans')) return 'FreeSans, sans-serif';
|
|
if (lowerName.includes('freeserif')) return 'FreeSerif, serif';
|
|
if (lowerName.includes('freemono')) return 'FreeMono, monospace';
|
|
|
|
// Default to font name
|
|
return fontName + ', sans-serif';
|
|
}
|
|
|
|
// Load fonts on page load
|
|
loadFonts();
|
|
|
|
// Load TV logos
|
|
async function loadTVLogos(search = '', country = '') {
|
|
try {
|
|
logoGrid.innerHTML = '<div class="loading-spinner">Loading logos...</div>';
|
|
|
|
const params = new URLSearchParams();
|
|
if (search) params.append('search', search);
|
|
if (country) params.append('country', country);
|
|
|
|
const response = await fetch(`/api/tv-logos?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
logoGrid.innerHTML = `<div style="padding: 20px; text-align: center; color: #c62828;">${data.error}</div>`;
|
|
return;
|
|
}
|
|
|
|
allLogos = data.logos;
|
|
|
|
// Populate country filter if not already done
|
|
if (allCountries.length === 0 && data.countries) {
|
|
allCountries = data.countries;
|
|
data.countries.forEach(country => {
|
|
const option = document.createElement('option');
|
|
option.value = country;
|
|
option.textContent = country;
|
|
countryFilter.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Reset display count and render first batch
|
|
displayedCount = 50;
|
|
currentFilteredLogos = allLogos;
|
|
renderLogos(allLogos);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load logos:', error);
|
|
logoGrid.innerHTML = '<div style="padding: 20px; text-align: center; color: #c62828;">Failed to load logos. Please try again.</div>';
|
|
}
|
|
}
|
|
|
|
function renderLogos(logos) {
|
|
currentFilteredLogos = logos;
|
|
displayedCount = 50;
|
|
|
|
if (logos.length === 0) {
|
|
logoGrid.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No logos found.</div>';
|
|
return;
|
|
}
|
|
|
|
logoGrid.innerHTML = '';
|
|
|
|
// Only render first batch
|
|
const logosToShow = logos.slice(0, displayedCount);
|
|
|
|
logosToShow.forEach(logo => {
|
|
const logoItem = document.createElement('div');
|
|
logoItem.className = 'logo-item';
|
|
logoItem.innerHTML = `
|
|
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
|
|
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
|
|
`;
|
|
|
|
logoItem.addEventListener('click', () => {
|
|
// Remove selected class from all items
|
|
document.querySelectorAll('.logo-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// Add selected class to clicked item
|
|
logoItem.classList.add('selected');
|
|
|
|
// Store the selected logo URL
|
|
selectedLogoUrl.value = logo.url;
|
|
|
|
// Manually trigger live preview since programmatic changes don't fire 'change' event
|
|
triggerLivePreview();
|
|
});
|
|
|
|
logoGrid.appendChild(logoItem);
|
|
});
|
|
|
|
// Add load more indicator if there are more logos
|
|
if (logosToShow.length < logos.length) {
|
|
const loadMore = document.createElement('div');
|
|
loadMore.id = 'loadMoreIndicator';
|
|
loadMore.style.cssText = 'grid-column: 1 / -1; padding: 20px; text-align: center; color: #999; font-size: 14px;';
|
|
loadMore.textContent = `Showing ${logosToShow.length} of ${logos.length} logos - scroll for more`;
|
|
logoGrid.appendChild(loadMore);
|
|
}
|
|
}
|
|
|
|
function loadMoreLogos() {
|
|
if (isLoadingMore || displayedCount >= currentFilteredLogos.length) {
|
|
return;
|
|
}
|
|
|
|
isLoadingMore = true;
|
|
|
|
// Get next batch
|
|
const nextBatch = currentFilteredLogos.slice(displayedCount, displayedCount + 50);
|
|
displayedCount += 50;
|
|
|
|
// Remove load more indicator
|
|
const loadMoreIndicator = document.getElementById('loadMoreIndicator');
|
|
if (loadMoreIndicator) {
|
|
loadMoreIndicator.remove();
|
|
}
|
|
|
|
// Append new logos
|
|
nextBatch.forEach(logo => {
|
|
const logoItem = document.createElement('div');
|
|
logoItem.className = 'logo-item';
|
|
logoItem.innerHTML = `
|
|
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
|
|
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
|
|
`;
|
|
|
|
logoItem.addEventListener('click', () => {
|
|
document.querySelectorAll('.logo-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
logoItem.classList.add('selected');
|
|
selectedLogoUrl.value = logo.url;
|
|
|
|
// Manually trigger live preview since programmatic changes don't fire 'change' event
|
|
triggerLivePreview();
|
|
});
|
|
|
|
logoGrid.appendChild(logoItem);
|
|
});
|
|
|
|
// Add load more indicator if there are still more
|
|
if (displayedCount < currentFilteredLogos.length) {
|
|
const loadMore = document.createElement('div');
|
|
loadMore.id = 'loadMoreIndicator';
|
|
loadMore.style.cssText = 'grid-column: 1 / -1; padding: 20px; text-align: center; color: #999; font-size: 14px;';
|
|
loadMore.textContent = `Showing ${displayedCount} of ${currentFilteredLogos.length} logos - scroll for more`;
|
|
logoGrid.appendChild(loadMore);
|
|
}
|
|
|
|
isLoadingMore = false;
|
|
}
|
|
|
|
// Debounce function for search
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Search and filter handlers
|
|
const debouncedSearch = debounce(() => {
|
|
const search = logoSearch.value;
|
|
const country = countryFilter.value;
|
|
|
|
// Filter logos client-side for instant results (searches ALL logos)
|
|
let filtered = allLogos;
|
|
|
|
// Filter by filename only
|
|
if (search) {
|
|
const searchLower = search.toLowerCase();
|
|
filtered = filtered.filter(logo =>
|
|
logo.name.toLowerCase().includes(searchLower)
|
|
);
|
|
}
|
|
|
|
// Filter by country
|
|
if (country) {
|
|
filtered = filtered.filter(logo => logo.country === country);
|
|
}
|
|
|
|
// Reset display count and render (shows first 50)
|
|
renderLogos(filtered);
|
|
}, 300);
|
|
|
|
logoSearch.addEventListener('input', debouncedSearch);
|
|
countryFilter.addEventListener('change', debouncedSearch);
|
|
|
|
// Tab switching
|
|
tabBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
activeInputMode = tab;
|
|
|
|
// Update button states
|
|
tabBtns.forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
|
|
// Update tab content
|
|
fileTab.classList.remove('active');
|
|
urlTab.classList.remove('active');
|
|
browseTab.classList.remove('active');
|
|
|
|
if (tab === 'file') {
|
|
fileTab.classList.add('active');
|
|
} else if (tab === 'url') {
|
|
urlTab.classList.add('active');
|
|
} else if (tab === 'browse') {
|
|
browseTab.classList.add('active');
|
|
// Load logos when browse tab is opened for the first time
|
|
if (allLogos.length === 0) {
|
|
loadTVLogos();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Infinite scroll for Browse Logos tab
|
|
logoGrid.addEventListener('scroll', () => {
|
|
const scrollTop = logoGrid.scrollTop;
|
|
const scrollHeight = logoGrid.scrollHeight;
|
|
const clientHeight = logoGrid.clientHeight;
|
|
|
|
// Load more when user scrolls to 80% of the content
|
|
if (scrollTop + clientHeight >= scrollHeight * 0.8) {
|
|
loadMoreLogos();
|
|
}
|
|
});
|
|
|
|
// Color picker sync
|
|
const textColorInput = document.getElementById('textColor');
|
|
const textColorPicker = document.getElementById('textColorPicker');
|
|
const bgColorInput = document.getElementById('bgColor');
|
|
const bgColorPicker = document.getElementById('bgColorPicker');
|
|
|
|
textColorPicker.addEventListener('input', (e) => {
|
|
textColorInput.value = e.target.value;
|
|
});
|
|
|
|
textColorInput.addEventListener('input', (e) => {
|
|
if (e.target.value.startsWith('#')) {
|
|
textColorPicker.value = e.target.value;
|
|
}
|
|
});
|
|
|
|
bgColorPicker.addEventListener('input', (e) => {
|
|
bgColorInput.value = e.target.value;
|
|
});
|
|
|
|
bgColorInput.addEventListener('input', (e) => {
|
|
if (e.target.value.startsWith('#') && e.target.value.length === 7) {
|
|
bgColorPicker.value = e.target.value;
|
|
}
|
|
});
|
|
|
|
// Font size and padding auto toggle
|
|
const fontSizeInput = document.getElementById('fontSize');
|
|
const fontSizeAuto = document.getElementById('fontSizeAuto');
|
|
const fontSizeSlider = document.getElementById('fontSizeSlider');
|
|
const paddingInput = document.getElementById('padding');
|
|
const paddingAuto = document.getElementById('paddingAuto');
|
|
const paddingSlider = document.getElementById('paddingSlider');
|
|
|
|
// Sync slider with number input
|
|
fontSizeSlider.addEventListener('input', (e) => {
|
|
fontSizeInput.value = e.target.value;
|
|
});
|
|
|
|
fontSizeInput.addEventListener('input', (e) => {
|
|
fontSizeSlider.value = e.target.value;
|
|
});
|
|
|
|
paddingSlider.addEventListener('input', (e) => {
|
|
paddingInput.value = e.target.value;
|
|
});
|
|
|
|
paddingInput.addEventListener('input', (e) => {
|
|
paddingSlider.value = e.target.value;
|
|
});
|
|
|
|
fontSizeAuto.addEventListener('change', (e) => {
|
|
fontSizeInput.disabled = e.target.checked;
|
|
fontSizeSlider.disabled = e.target.checked;
|
|
if (e.target.checked) {
|
|
fontSizeInput.style.opacity = '0.5';
|
|
fontSizeSlider.style.opacity = '0.5';
|
|
} else {
|
|
fontSizeInput.style.opacity = '1';
|
|
fontSizeSlider.style.opacity = '1';
|
|
document.getElementById('usedFontSizeDisplay').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
paddingAuto.addEventListener('change', (e) => {
|
|
paddingInput.disabled = e.target.checked;
|
|
paddingSlider.disabled = e.target.checked;
|
|
if (e.target.checked) {
|
|
paddingInput.style.opacity = '0.5';
|
|
paddingSlider.style.opacity = '0.5';
|
|
} else {
|
|
paddingInput.style.opacity = '1';
|
|
paddingSlider.style.opacity = '1';
|
|
document.getElementById('usedPaddingDisplay').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Initialize disabled state
|
|
fontSizeInput.disabled = fontSizeAuto.checked;
|
|
fontSizeSlider.disabled = fontSizeAuto.checked;
|
|
paddingInput.disabled = paddingAuto.checked;
|
|
paddingSlider.disabled = paddingAuto.checked;
|
|
if (fontSizeAuto.checked) {
|
|
fontSizeInput.style.opacity = '0.5';
|
|
fontSizeSlider.style.opacity = '0.5';
|
|
}
|
|
if (paddingAuto.checked) {
|
|
paddingInput.style.opacity = '0.5';
|
|
paddingSlider.style.opacity = '0.5';
|
|
}
|
|
|
|
// Dimension overlay toggle and update
|
|
function updateDimensionOverlay() {
|
|
if (!showDimensions.checked || !previewImage.dataset.fontSize) {
|
|
dimensionOverlay.style.display = 'none';
|
|
fontSizeIndicator.style.display = 'none';
|
|
paddingIndicator.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
dimensionOverlay.style.display = 'block';
|
|
|
|
const fontSize = previewImage.dataset.fontSize;
|
|
const padding = previewImage.dataset.padding;
|
|
const position = document.querySelector('input[name="position"]:checked').value;
|
|
const originalWidth = parseInt(previewImage.dataset.originalWidth);
|
|
const originalHeight = parseInt(previewImage.dataset.originalHeight);
|
|
|
|
const imgWidth = previewImage.offsetWidth;
|
|
const imgHeight = previewImage.offsetHeight;
|
|
const naturalWidth = previewImage.naturalWidth;
|
|
const naturalHeight = previewImage.naturalHeight;
|
|
const scale = imgWidth / naturalWidth;
|
|
|
|
// Use API to get calculated values if auto, otherwise use provided values
|
|
const updateVisualization = async () => {
|
|
let fontSizeNum, paddingNum;
|
|
|
|
if (fontSize === 'auto' || padding === 'auto') {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
width: originalWidth,
|
|
height: originalHeight,
|
|
position: position,
|
|
font_size: fontSize,
|
|
padding: padding
|
|
});
|
|
|
|
const response = await fetch(`/api/calculate-auto-values?${params}`);
|
|
const data = await response.json();
|
|
|
|
fontSizeNum = fontSize === 'auto' ? data.font_size : parseInt(fontSize);
|
|
paddingNum = padding === 'auto' ? data.padding : parseInt(padding);
|
|
} catch (error) {
|
|
console.error('Failed to calculate auto values:', error);
|
|
// Fallback to manual values if API fails
|
|
fontSizeNum = parseInt(fontSize) || 60;
|
|
paddingNum = parseInt(padding) || 20;
|
|
}
|
|
} else {
|
|
fontSizeNum = parseInt(fontSize);
|
|
paddingNum = parseInt(padding);
|
|
}
|
|
|
|
fontSizeLabel.textContent = `Font: ${fontSize === 'auto' ? `~${fontSizeNum} (auto)` : fontSize}px`;
|
|
paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`;
|
|
|
|
// Backend uses inner_padding (full padding) and outer_padding (padding/4)
|
|
const innerPadding = paddingNum * scale;
|
|
const outerPadding = (paddingNum / 4) * scale;
|
|
|
|
// For horizontal text (left/right positions), we need to estimate actual text width
|
|
// Use canvas to measure text width more accurately
|
|
const text = document.getElementById('text').value;
|
|
let textWidth, textHeight;
|
|
|
|
if (position === 'left' || position === 'right') {
|
|
// For left/right: use actual text width and actual bounding box height
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.font = `${fontSizeNum}px Arial`;
|
|
const metrics = ctx.measureText(text);
|
|
textWidth = metrics.width * scale;
|
|
// Use actual text bounding box height (tighter than font size)
|
|
textHeight = fontSizeNum * 0.75 * scale;
|
|
} else {
|
|
// For above/below: text height is tighter bounding box
|
|
textHeight = fontSizeNum * 0.75 * scale;
|
|
textWidth = fontSizeNum * scale; // Not used in above/below
|
|
}
|
|
|
|
// Calculate text_area dimensions (matches backend exactly)
|
|
let textAreaHeight, textAreaWidth;
|
|
if (position === 'below' || position === 'above') {
|
|
textAreaHeight = textHeight + outerPadding + innerPadding;
|
|
} else {
|
|
textAreaWidth = textWidth + outerPadding + innerPadding;
|
|
}
|
|
|
|
// Calculate where the original logo is positioned in the final image
|
|
const scaledOriginalWidth = originalWidth * scale;
|
|
const scaledOriginalHeight = originalHeight * scale;
|
|
|
|
// Reset all styles completely
|
|
fontSizeIndicator.style.cssText = 'position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: block;';
|
|
paddingIndicator.style.cssText = 'position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: block;';
|
|
fontSizeLabel.style.cssText = 'position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
|
|
paddingLabel.style.cssText = 'position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
|
|
|
|
if (position === 'below') {
|
|
// Layout: [logo at y=0] [innerPadding] [text] [outerPadding]
|
|
|
|
// Blue box: shows ONLY the text bounding box - FULL WIDTH
|
|
fontSizeIndicator.style.left = '0px';
|
|
fontSizeIndicator.style.right = '0px';
|
|
fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`;
|
|
fontSizeIndicator.style.height = `${textHeight}px`;
|
|
|
|
// Font label on the LEFT side, anchored to BOTTOM edge of text box
|
|
fontSizeLabel.style.right = '100%';
|
|
fontSizeLabel.style.bottom = '0px';
|
|
fontSizeLabel.style.marginRight = '8px';
|
|
|
|
// Green box: shows ONLY innerPadding - FULL WIDTH
|
|
paddingIndicator.style.left = '0px';
|
|
paddingIndicator.style.right = '0px';
|
|
paddingIndicator.style.top = `${scaledOriginalHeight}px`;
|
|
paddingIndicator.style.height = `${innerPadding}px`;
|
|
|
|
// Padding label on the RIGHT side, anchored to BOTTOM edge of padding box
|
|
paddingLabel.style.left = '100%';
|
|
paddingLabel.style.bottom = '0px';
|
|
paddingLabel.style.marginLeft = '8px';
|
|
|
|
} else if (position === 'above') {
|
|
// Layout: [outerPadding] [text] [innerPadding] [logo]
|
|
|
|
// Blue box: shows ONLY the text bounding box - FULL WIDTH
|
|
fontSizeIndicator.style.left = '0px';
|
|
fontSizeIndicator.style.right = '0px';
|
|
fontSizeIndicator.style.top = `${outerPadding}px`;
|
|
fontSizeIndicator.style.height = `${textHeight}px`;
|
|
|
|
// Font label on the LEFT side, anchored to TOP edge of text box
|
|
fontSizeLabel.style.right = '100%';
|
|
fontSizeLabel.style.top = '0px';
|
|
fontSizeLabel.style.marginRight = '8px';
|
|
|
|
// Green box: shows ONLY innerPadding - FULL WIDTH
|
|
paddingIndicator.style.left = '0px';
|
|
paddingIndicator.style.right = '0px';
|
|
paddingIndicator.style.top = `${outerPadding + textHeight}px`;
|
|
paddingIndicator.style.height = `${innerPadding}px`;
|
|
|
|
// Padding label on the RIGHT side, anchored to TOP edge of padding box
|
|
paddingLabel.style.left = '100%';
|
|
paddingLabel.style.top = '0px';
|
|
paddingLabel.style.marginLeft = '8px';
|
|
|
|
} else if (position === 'right') {
|
|
// Layout: [logo at x=0] [innerPadding] [text] [outerPadding]
|
|
|
|
// Blue box: shows ONLY the text width - FULL HEIGHT
|
|
fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`;
|
|
fontSizeIndicator.style.top = '0px';
|
|
fontSizeIndicator.style.bottom = '0px';
|
|
fontSizeIndicator.style.width = `${textWidth}px`;
|
|
|
|
// Font label ABOVE the box, anchored to RIGHT edge of text box
|
|
fontSizeLabel.style.right = '0px';
|
|
fontSizeLabel.style.bottom = '100%';
|
|
fontSizeLabel.style.marginBottom = '8px';
|
|
|
|
// Green box: shows ONLY innerPadding - FULL HEIGHT
|
|
paddingIndicator.style.left = `${scaledOriginalWidth}px`;
|
|
paddingIndicator.style.top = '0px';
|
|
paddingIndicator.style.bottom = '0px';
|
|
paddingIndicator.style.width = `${innerPadding}px`;
|
|
|
|
// Padding label BELOW the box, anchored to RIGHT edge of padding box
|
|
paddingLabel.style.right = '0px';
|
|
paddingLabel.style.top = '100%';
|
|
paddingLabel.style.marginTop = '8px';
|
|
|
|
} else {
|
|
// position === 'left'
|
|
// Layout: [outerPadding] [text] [innerPadding] [logo]
|
|
|
|
// Blue box: shows ONLY the text width - FULL HEIGHT
|
|
fontSizeIndicator.style.left = `${outerPadding}px`;
|
|
fontSizeIndicator.style.top = '0px';
|
|
fontSizeIndicator.style.bottom = '0px';
|
|
fontSizeIndicator.style.width = `${textWidth}px`;
|
|
|
|
// Font label ABOVE the box, anchored to LEFT edge of text box
|
|
fontSizeLabel.style.left = '0px';
|
|
fontSizeLabel.style.bottom = '100%';
|
|
fontSizeLabel.style.marginBottom = '8px';
|
|
|
|
// Green box: shows ONLY innerPadding - FULL HEIGHT
|
|
paddingIndicator.style.left = `${outerPadding + textWidth}px`;
|
|
paddingIndicator.style.top = '0px';
|
|
paddingIndicator.style.bottom = '0px';
|
|
paddingIndicator.style.width = `${innerPadding}px`;
|
|
|
|
// Padding label BELOW the box, anchored to LEFT edge of padding box
|
|
paddingLabel.style.left = '0px';
|
|
paddingLabel.style.top = '100%';
|
|
paddingLabel.style.marginTop = '8px';
|
|
}
|
|
};
|
|
|
|
updateVisualization();
|
|
}
|
|
|
|
showDimensions.addEventListener('change', updateDimensionOverlay);
|
|
previewImage.addEventListener('load', updateDimensionOverlay);
|
|
window.addEventListener('resize', updateDimensionOverlay);
|
|
|
|
// Update visualization when position changes
|
|
document.querySelectorAll('input[name="position"]').forEach(radio => {
|
|
radio.addEventListener('change', updateDimensionOverlay);
|
|
});
|
|
|
|
// Click preview image to download
|
|
previewImage.addEventListener('click', () => {
|
|
if (previewImage.dataset.downloadUrl) {
|
|
const a = document.createElement('a');
|
|
a.href = previewImage.dataset.downloadUrl;
|
|
a.download = previewImage.dataset.downloadName || 'logo.png';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
});
|
|
|
|
// File input handling
|
|
imageFile.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
fileLabel.classList.add('has-file');
|
|
fileLabel.innerHTML = `<span>✅ ${file.name}</span>`;
|
|
} else {
|
|
fileLabel.classList.remove('has-file');
|
|
fileLabel.innerHTML = '<span>📁 Choose an image file (PNG, JPG, GIF, WEBP)</span>';
|
|
}
|
|
});
|
|
|
|
function showMessage(message, type) {
|
|
messageDiv.innerHTML = `<div class="${type}">${message}</div>`;
|
|
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
function clearMessage() {
|
|
messageDiv.innerHTML = '';
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const text = document.getElementById('text').value;
|
|
if (!text.trim()) {
|
|
return;
|
|
}
|
|
|
|
const position = document.querySelector('input[name="position"]:checked').value;
|
|
const fontSizeAuto = document.getElementById('fontSizeAuto').checked;
|
|
const fontSize = fontSizeAuto ? 'auto' : document.getElementById('fontSize').value;
|
|
const paddingAuto = document.getElementById('paddingAuto').checked;
|
|
const padding = paddingAuto ? 'auto' : document.getElementById('padding').value;
|
|
const textColor = textColorInput.value;
|
|
const bgColor = bgColorInput.value;
|
|
const fontPath = fontSelect.value;
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = '⏳ Processing...';
|
|
|
|
// Show loading state
|
|
if (preview.style.display === 'block') {
|
|
previewImage.classList.add('loading');
|
|
previewLoadingOverlay.classList.add('active');
|
|
} else {
|
|
preview.style.display = 'block';
|
|
previewLoadingOverlay.classList.add('active');
|
|
}
|
|
|
|
try {
|
|
let response;
|
|
let originalImageWidth = null;
|
|
let originalImageHeight = null;
|
|
|
|
if (activeInputMode === 'browse') {
|
|
// Use selected logo from browse
|
|
const url = selectedLogoUrl.value.trim();
|
|
if (!url) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '✨ Generate Logo';
|
|
return;
|
|
}
|
|
|
|
// Get original dimensions from the selected logo
|
|
const selectedLogoImg = document.querySelector(`.logo-item.selected img`);
|
|
if (selectedLogoImg && selectedLogoImg.complete) {
|
|
originalImageWidth = selectedLogoImg.naturalWidth;
|
|
originalImageHeight = selectedLogoImg.naturalHeight;
|
|
}
|
|
|
|
const params = new URLSearchParams({
|
|
url: url,
|
|
text: text,
|
|
position: position,
|
|
font_size: fontSize,
|
|
padding: padding,
|
|
text_color: textColor,
|
|
bg_color: bgColor,
|
|
font_path: fontPath
|
|
});
|
|
|
|
response = await fetch(`/api/image?${params}`);
|
|
} else if (activeInputMode === 'url') {
|
|
// Use URL-based API
|
|
const url = imageUrl.value.trim();
|
|
if (!url) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '✨ Generate Logo';
|
|
return;
|
|
}
|
|
|
|
// Try to get dimensions from URL image
|
|
try {
|
|
const tempImg = new Image();
|
|
tempImg.crossOrigin = "anonymous";
|
|
await new Promise((resolve, reject) => {
|
|
tempImg.onload = resolve;
|
|
tempImg.onerror = () => resolve(); // Continue even if CORS fails
|
|
tempImg.src = url;
|
|
setTimeout(() => resolve(), 1000); // Timeout after 1s
|
|
});
|
|
if (tempImg.complete && tempImg.naturalWidth) {
|
|
originalImageWidth = tempImg.naturalWidth;
|
|
originalImageHeight = tempImg.naturalHeight;
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors, continue without dimensions
|
|
}
|
|
|
|
const params = new URLSearchParams({
|
|
url: url,
|
|
text: text,
|
|
position: position,
|
|
font_size: fontSize,
|
|
padding: padding,
|
|
text_color: textColor,
|
|
bg_color: bgColor,
|
|
font_path: fontPath
|
|
});
|
|
|
|
response = await fetch(`/api/image?${params}`);
|
|
} else {
|
|
// Use file upload API
|
|
const file = imageFile.files[0];
|
|
if (!file) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '✨ Generate Logo';
|
|
return;
|
|
}
|
|
|
|
// Get dimensions from file
|
|
try {
|
|
const tempImg = new Image();
|
|
const reader = new FileReader();
|
|
await new Promise((resolve) => {
|
|
reader.onload = (e) => {
|
|
tempImg.onload = () => {
|
|
originalImageWidth = tempImg.naturalWidth;
|
|
originalImageHeight = tempImg.naturalHeight;
|
|
resolve();
|
|
};
|
|
tempImg.src = e.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
} catch (e) {
|
|
// Continue without dimensions
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
formData.append('text', text);
|
|
formData.append('position', position);
|
|
formData.append('font_size', fontSize);
|
|
formData.append('padding', padding);
|
|
formData.append('text_color', textColor);
|
|
formData.append('bg_color', bgColor);
|
|
formData.append('font_path', fontPath);
|
|
|
|
response = await fetch('/api/process', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to process image');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Read metadata from response headers
|
|
const fontSizeUsed = response.headers.get('X-Font-Size-Used');
|
|
const paddingUsed = response.headers.get('X-Padding-Used');
|
|
const fontNameUsed = response.headers.get('X-Font-Name-Used');
|
|
|
|
// Update input fields and sliders if auto mode is enabled
|
|
const usedFontSizeDisplay = document.getElementById('usedFontSizeDisplay');
|
|
const usedPaddingDisplay = document.getElementById('usedPaddingDisplay');
|
|
const usedFontDisplay = document.getElementById('usedFontDisplay');
|
|
|
|
if (fontSizeUsed && fontSizeAuto.checked) {
|
|
fontSizeInput.value = fontSizeUsed;
|
|
fontSizeSlider.value = fontSizeUsed;
|
|
usedFontSizeDisplay.textContent = `Using: ${fontSizeUsed}px`;
|
|
usedFontSizeDisplay.style.display = 'block';
|
|
} else {
|
|
usedFontSizeDisplay.style.display = 'none';
|
|
}
|
|
|
|
if (paddingUsed && paddingAuto.checked) {
|
|
paddingInput.value = paddingUsed;
|
|
paddingSlider.value = paddingUsed;
|
|
usedPaddingDisplay.textContent = `Using: ${paddingUsed}px`;
|
|
usedPaddingDisplay.style.display = 'block';
|
|
} else {
|
|
usedPaddingDisplay.style.display = 'none';
|
|
}
|
|
|
|
if (fontNameUsed && fontSelect.value === 'auto') {
|
|
usedFontDisplay.textContent = `Using: ${fontNameUsed}`;
|
|
usedFontDisplay.style.display = 'block';
|
|
} else {
|
|
usedFontDisplay.style.display = 'none';
|
|
}
|
|
|
|
// Set image and wait for it to load before hiding spinner
|
|
previewImage.onload = () => {
|
|
previewImage.classList.remove('loading');
|
|
previewLoadingOverlay.classList.remove('active');
|
|
};
|
|
|
|
previewImage.src = url;
|
|
previewImage.dataset.downloadUrl = url;
|
|
previewImage.dataset.downloadName = `logo_${text.replace(/\s+/g, '_')}.png`;
|
|
preview.style.display = 'block';
|
|
|
|
// Store the actual values used for dimension overlay
|
|
previewImage.dataset.fontSize = fontSize;
|
|
previewImage.dataset.padding = padding;
|
|
previewImage.dataset.position = position;
|
|
|
|
// Store original dimensions if available
|
|
if (originalImageWidth && originalImageHeight) {
|
|
previewImage.dataset.originalWidth = originalImageWidth;
|
|
previewImage.dataset.originalHeight = originalImageHeight;
|
|
} else {
|
|
// Fallback - estimate from processed image natural dimensions
|
|
// This is approximate but better than nothing
|
|
previewImage.dataset.originalWidth = previewImage.naturalWidth;
|
|
previewImage.dataset.originalHeight = previewImage.naturalHeight;
|
|
}
|
|
|
|
preview.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
// Update dimension overlay after image loads
|
|
setTimeout(updateDimensionOverlay, 100);
|
|
|
|
} catch (error) {
|
|
showMessage(`❌ Error: ${error.message}`, 'error');
|
|
previewImage.classList.remove('loading');
|
|
previewLoadingOverlay.classList.remove('active');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '✨ Generate Logo';
|
|
}
|
|
});
|
|
|
|
// Live preview functionality (always enabled)
|
|
let generateTimeout = null;
|
|
|
|
function triggerLivePreview() {
|
|
clearTimeout(generateTimeout);
|
|
generateTimeout = setTimeout(() => {
|
|
// Trigger form submission programmatically
|
|
const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
|
|
form.dispatchEvent(submitEvent);
|
|
}, 800); // 800ms debounce
|
|
}
|
|
|
|
// Add live preview listeners to all form inputs
|
|
document.getElementById('text').addEventListener('input', triggerLivePreview);
|
|
document.querySelectorAll('input[name="position"]').forEach(radio => {
|
|
radio.addEventListener('change', triggerLivePreview);
|
|
});
|
|
fontSizeInput.addEventListener('input', triggerLivePreview);
|
|
fontSizeSlider.addEventListener('input', triggerLivePreview);
|
|
fontSizeAuto.addEventListener('change', triggerLivePreview);
|
|
paddingInput.addEventListener('input', triggerLivePreview);
|
|
paddingSlider.addEventListener('input', triggerLivePreview);
|
|
paddingAuto.addEventListener('change', triggerLivePreview);
|
|
textColorInput.addEventListener('input', triggerLivePreview);
|
|
textColorPicker.addEventListener('input', triggerLivePreview);
|
|
bgColorInput.addEventListener('input', triggerLivePreview);
|
|
bgColorPicker.addEventListener('input', triggerLivePreview);
|
|
fontSelect.addEventListener('change', triggerLivePreview);
|
|
imageUrl.addEventListener('input', triggerLivePreview);
|
|
selectedLogoUrl.addEventListener('change', triggerLivePreview);
|
|
|
|
// Trigger on file selection
|
|
imageFile.addEventListener('change', () => {
|
|
setTimeout(triggerLivePreview, 100);
|
|
});
|
|
|
|
// API URL generation and updating
|
|
const apiUrlInput = document.getElementById('apiUrl');
|
|
|
|
function updateApiUrl() {
|
|
const text = document.getElementById('text').value;
|
|
|
|
// Don't show API URL if no text or no image source
|
|
if (!text.trim()) {
|
|
apiUrlInput.value = '';
|
|
return;
|
|
}
|
|
|
|
let imageUrl = '';
|
|
|
|
// Get the image URL based on active input mode
|
|
if (activeInputMode === 'browse') {
|
|
imageUrl = selectedLogoUrl.value.trim();
|
|
} else if (activeInputMode === 'url') {
|
|
imageUrl = document.getElementById('imageUrl').value.trim();
|
|
} else {
|
|
// File upload mode - can't generate API URL
|
|
apiUrlInput.value = 'API URL only available for URL/Browse modes (not file upload)';
|
|
return;
|
|
}
|
|
|
|
if (!imageUrl) {
|
|
apiUrlInput.value = '';
|
|
return;
|
|
}
|
|
|
|
// Build API URL
|
|
const position = document.querySelector('input[name="position"]:checked').value;
|
|
const fontSizeAutoChecked = document.getElementById('fontSizeAuto').checked;
|
|
const fontSize = fontSizeAutoChecked ? 'auto' : document.getElementById('fontSize').value;
|
|
const paddingAutoChecked = document.getElementById('paddingAuto').checked;
|
|
const padding = paddingAutoChecked ? 'auto' : document.getElementById('padding').value;
|
|
const textColor = textColorInput.value;
|
|
const bgColor = bgColorInput.value;
|
|
const fontPath = fontSelect.value;
|
|
|
|
const params = new URLSearchParams({
|
|
url: imageUrl,
|
|
text: text,
|
|
position: position,
|
|
font_size: fontSize,
|
|
padding: padding,
|
|
text_color: textColor,
|
|
bg_color: bgColor
|
|
});
|
|
|
|
// Only include font_path if it's not 'auto' (since auto is the default)
|
|
if (fontPath && fontPath !== 'auto') {
|
|
params.append('font_path', fontPath);
|
|
}
|
|
|
|
// Build full URL (use current origin)
|
|
const baseUrl = window.location.origin;
|
|
apiUrlInput.value = `${baseUrl}/api/image?${params.toString()}`;
|
|
}
|
|
|
|
// Update API URL whenever relevant inputs change
|
|
document.getElementById('text').addEventListener('input', updateApiUrl);
|
|
document.querySelectorAll('input[name="position"]').forEach(radio => {
|
|
radio.addEventListener('change', updateApiUrl);
|
|
});
|
|
fontSizeInput.addEventListener('input', updateApiUrl);
|
|
fontSizeSlider.addEventListener('input', updateApiUrl);
|
|
fontSizeAuto.addEventListener('change', updateApiUrl);
|
|
paddingInput.addEventListener('input', updateApiUrl);
|
|
paddingSlider.addEventListener('input', updateApiUrl);
|
|
paddingAuto.addEventListener('change', updateApiUrl);
|
|
textColorInput.addEventListener('input', updateApiUrl);
|
|
textColorPicker.addEventListener('input', updateApiUrl);
|
|
bgColorInput.addEventListener('input', updateApiUrl);
|
|
bgColorPicker.addEventListener('input', updateApiUrl);
|
|
fontSelect.addEventListener('change', updateApiUrl);
|
|
imageUrl.addEventListener('input', updateApiUrl);
|
|
selectedLogoUrl.addEventListener('change', updateApiUrl);
|
|
|
|
// Update when tab changes
|
|
tabBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
setTimeout(updateApiUrl, 100);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|