Jaimodiji's picture
Upload folder using huggingface_hub
c001f24
{% extends "base.html" %}
{% block title %}Resize PDF - DocuPDF{% endblock %}
{% block styles %}
<style>
/* === MOBILE-FIRST RESPONSIVE LAYOUT === */
/* Prevent body scroll when modal is open */
body.modal-open {
overflow: hidden;
}
/* Main container - fills viewport properly */
.resize-container {
height: calc(100vh - 56px); /* Subtract navbar height */
height: calc(100dvh - 56px); /* Dynamic viewport height for mobile */
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Card takes full height */
.resize-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin: 0.5rem;
border-radius: 0.5rem;
}
.resize-card .card-header {
flex-shrink: 0;
padding: 0.75rem 1rem;
}
.resize-card .card-header h2 {
font-size: 1.25rem;
margin: 0;
}
.resize-card .card-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}
/* === PANEL LAYOUT === */
.panels-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
@media (min-width: 992px) {
.panels-container {
flex-direction: row;
}
}
.panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0; /* Critical for flex children to scroll */
}
.panel-header {
flex-shrink: 0;
padding: 0.75rem 1rem;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-header h5 {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
/* === SCROLLABLE CONTENT - TOUCH OPTIMIZED === */
.panel-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
/* Enable smooth touch scrolling */
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
/* Hide scrollbar but keep functionality */
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.3) transparent;
}
.panel-content::-webkit-scrollbar {
width: 4px;
}
.panel-content::-webkit-scrollbar-track {
background: transparent;
}
.panel-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
/* === FILE BROWSER PANEL === */
.file-browser-panel {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
@media (min-width: 992px) {
.file-browser-panel {
border-bottom: none;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
}
/* Search bar */
.search-bar {
padding: 0.75rem 1rem;
background: rgba(0, 0, 0, 0.1);
}
.search-bar .input-group {
max-width: 100%;
}
.search-bar .form-control {
font-size: 16px; /* Prevents zoom on iOS */
}
/* Breadcrumb */
.breadcrumb-wrapper {
padding: 0.5rem 1rem;
background: rgba(0, 0, 0, 0.15);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
white-space: nowrap;
}
.breadcrumb {
margin: 0;
flex-wrap: nowrap;
font-size: 0.875rem;
}
/* File list */
.file-list {
padding: 0;
}
.file-item {
display: flex;
align-items: center;
padding: 0.875rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
transition: background-color 0.15s ease;
/* Larger touch target */
min-height: 48px;
/* Prevent text selection on touch */
-webkit-user-select: none;
user-select: none;
/* Touch feedback */
-webkit-tap-highlight-color: rgba(13, 110, 253, 0.3);
}
.file-item:active {
background-color: rgba(13, 110, 253, 0.2);
}
.file-item.selected {
background-color: #0d6efd !important;
}
.file-item .icon {
font-size: 1.25rem;
margin-right: 0.75rem;
flex-shrink: 0;
}
.file-item .filename {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9375rem;
}
.file-item .badge {
margin-left: 0.5rem;
flex-shrink: 0;
}
.file-item.folder .icon {
color: #ffc107;
}
.file-item.pdf .icon {
color: #dc3545;
}
/* Empty state */
.empty-state {
padding: 2rem;
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
/* === OPTIONS PANEL === */
.options-panel .panel-content {
padding: 1rem;
}
/* Form groups with better spacing */
.option-group {
margin-bottom: 1.25rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.option-group:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.option-group label,
.option-group .option-label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.9);
}
/* Radio/Checkbox - larger touch targets */
.option-choice {
display: flex;
align-items: center;
padding: 0.625rem 0.75rem;
margin: 0.25rem 0;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.15s;
/* Touch target */
min-height: 44px;
-webkit-tap-highlight-color: rgba(13, 110, 253, 0.2);
}
.option-choice:active {
background-color: rgba(255, 255, 255, 0.1);
}
.option-choice input {
width: 20px;
height: 20px;
margin-right: 0.75rem;
flex-shrink: 0;
}
.option-choice span {
font-size: 0.9375rem;
}
/* Sub-options (stitch direction) */
.sub-options {
margin-left: 1.5rem;
padding-left: 1rem;
border-left: 2px solid rgba(255, 255, 255, 0.2);
}
/* Color palette */
.color-palette {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.color-swatch {
width: 44px;
height: 44px;
border-radius: 0.5rem;
border: 3px solid transparent;
cursor: pointer;
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.color-swatch:active {
transform: scale(0.95);
}
.color-swatch.active {
border-color: #fff;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.5);
}
.color-custom-btn {
height: 44px;
padding: 0 1rem;
font-size: 0.875rem;
}
/* Pattern choices - horizontal on mobile */
.pattern-choices {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.pattern-choice {
flex: 1;
min-width: 80px;
text-align: center;
padding: 0.75rem;
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.15s;
-webkit-tap-highlight-color: transparent;
}
.pattern-choice:active {
background-color: rgba(255, 255, 255, 0.1);
}
.pattern-choice.active {
border-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.2);
}
.pattern-choice input {
display: none;
}
.pattern-choice .pattern-icon {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.pattern-choice .pattern-name {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* === SUBMIT BUTTON === */
.submit-section {
flex-shrink: 0;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.submit-btn {
width: 100%;
padding: 0.875rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border-radius: 0.5rem;
/* Touch optimization */
min-height: 50px;
}
/* Selected file indicator */
.selected-file-indicator {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
margin-bottom: 0.75rem;
background: rgba(13, 110, 253, 0.2);
border-radius: 0.375rem;
font-size: 0.875rem;
}
.selected-file-indicator .bi {
margin-right: 0.5rem;
color: #0d6efd;
}
.selected-file-indicator .filename {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-file-indicator .clear-btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
/* === COLOR PICKER MODAL === */
.color-picker-modal .modal-content {
border-radius: 1rem;
}
.color-picker-wrapper {
padding: 1rem;
}
.saturation-brightness-area {
width: 100%;
aspect-ratio: 1;
max-width: 280px;
margin: 0 auto;
border-radius: 0.5rem;
position: relative;
cursor: crosshair;
touch-action: none; /* Prevent scrolling while dragging */
}
.saturation-brightness-area .gradient-white {
position: absolute;
inset: 0;
background: linear-gradient(to right, #fff, transparent);
border-radius: inherit;
}
.saturation-brightness-area .gradient-black {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent, #000);
border-radius: inherit;
}
.sb-cursor {
position: absolute;
width: 20px;
height: 20px;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px #000, inset 0 0 0 1px #000;
transform: translate(-50%, -50%);
pointer-events: none;
}
.hue-slider-area {
width: 100%;
max-width: 280px;
height: 32px;
margin: 1rem auto 0;
border-radius: 1rem;
background: linear-gradient(to right,
#f00 0%, #ff0 17%, #0f0 33%,
#0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
position: relative;
cursor: pointer;
touch-action: none;
}
.hue-cursor {
position: absolute;
top: 50%;
width: 8px;
height: 120%;
background: #fff;
border: 2px solid #000;
border-radius: 4px;
transform: translate(-50%, -50%);
pointer-events: none;
}
.color-preview-row {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1rem;
max-width: 280px;
margin-left: auto;
margin-right: auto;
}
.color-preview-box {
width: 60px;
height: 60px;
border-radius: 0.5rem;
border: 2px solid rgba(255, 255, 255, 0.3);
flex-shrink: 0;
}
.color-hex-input {
flex: 1;
font-size: 16px; /* Prevent zoom on iOS */
text-transform: uppercase;
font-family: monospace;
}
/* === TABLET SPECIFIC ADJUSTMENTS === */
@media (min-width: 768px) and (max-width: 1199px) {
.panels-container {
flex-direction: row;
}
.file-browser-panel {
flex: 0 0 45%;
border-bottom: none;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.options-panel {
flex: 0 0 55%;
}
}
@media (max-width: 767px) {
/* Stack panels on small screens */
.file-browser-panel {
flex: 0 0 50%;
min-height: 40vh;
}
.options-panel {
flex: 1;
min-height: 0;
}
}
/* Landscape tablet */
@media (min-height: 600px) and (orientation: landscape) {
.resize-container {
height: calc(100vh - 56px);
height: calc(100dvh - 56px);
}
.panels-container {
flex-direction: row;
}
.file-browser-panel,
.options-panel {
flex: 1;
}
}
/* Very short screens */
@media (max-height: 500px) {
.resize-card .card-header {
padding: 0.5rem 1rem;
}
.resize-card .card-header h2 {
font-size: 1rem;
}
.option-group {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
}
}
</style>
{% endblock %}
{% block content %}
<div class="resize-container">
<div class="card bg-dark text-white resize-card">
<div class="card-header">
<h2><i class="bi bi-arrows-angle-expand me-2"></i>Resize PDF for Notes</h2>
</div>
<div class="card-body">
<form action="{{ url_for('main.resize_pdf_route', folder_path=breadcrumbs|map(attribute='path')|join('/')) }}"
method="post"
id="resize-form"
class="h-100 d-flex flex-column">
<div class="panels-container">
<!-- FILE BROWSER PANEL -->
<div class="panel file-browser-panel">
<div class="panel-header">
<h5><i class="bi bi-folder2-open me-2"></i>Select PDF File</h5>
</div>
<!-- Search -->
<div class="search-bar">
<div class="input-group">
<input type="text"
class="form-control"
name="search"
placeholder="Search PDFs..."
value="{{ search_query or '' }}"
autocomplete="off">
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i>
</button>
{% if search_query %}
<a href="{{ url_for('main.resize_pdf_route') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
{% endif %}
</div>
</div>
<!-- Breadcrumb -->
<div class="breadcrumb-wrapper">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="{{ url_for('main.resize_pdf_route') }}">
<i class="bi bi-house-fill"></i>
</a>
</li>
{% for item in breadcrumbs %}
<li class="breadcrumb-item">
<a href="{{ url_for('main.resize_pdf_route', folder_path=item.path) }}">{{ item.name }}</a>
</li>
{% endfor %}
</ol>
</nav>
</div>
<!-- File List -->
<div class="panel-content file-list" id="file-list">
{% if not subfolders and not pdfs %}
<div class="empty-state">
<i class="bi bi-folder-x" style="font-size: 3rem;"></i>
<p class="mt-2 mb-0">No files found</p>
</div>
{% else %}
{% for folder in subfolders %}
<div class="file-item folder" data-path="{{ (breadcrumbs|map(attribute='path')|list)|last if breadcrumbs else '' }}/{{ folder.name }}">
<i class="bi bi-folder-fill icon"></i>
<span class="filename">{{ folder.name }}</span>
<i class="bi bi-chevron-right text-muted"></i>
</div>
{% endfor %}
{% for pdf in pdfs %}
<div class="file-item pdf" data-filename="{{ pdf.filename }}">
<i class="bi bi-file-earmark-pdf-fill icon"></i>
<span class="filename">{{ pdf.filename }}</span>
{% if pdf.persist %}
<span class="badge bg-info"><i class="bi bi-pin-fill"></i></span>
{% endif %}
</div>
{% endfor %}
{% endif %}
</div>
<input type="hidden" name="input_pdf" id="input_pdf">
</div>
<!-- OPTIONS PANEL -->
<div class="panel options-panel">
<div class="panel-header">
<h5><i class="bi bi-gear me-2"></i>Options</h5>
</div>
<div class="panel-content">
<!-- Selected File Indicator -->
<div class="selected-file-indicator" id="selected-indicator" style="display: none;">
<i class="bi bi-file-earmark-pdf-fill"></i>
<span class="filename" id="selected-filename"></span>
<button type="button" class="btn btn-outline-danger btn-sm clear-btn" id="clear-selection">
<i class="bi bi-x"></i>
</button>
</div>
<!-- Output Filename -->
<div class="option-group">
<label for="output_pdf">Output Filename</label>
<input type="text"
class="form-control"
id="output_pdf"
name="output_pdf"
placeholder="Select a PDF first..."
required>
</div>
<!-- Processing Mode -->
<div class="option-group">
<span class="option-label">Processing Mode</span>
<label class="option-choice">
<input type="radio" name="mode" value="notes_only" checked>
<span>Add notes area to full page</span>
</label>
<label class="option-choice">
<input type="radio" name="mode" value="split">
<span>Split page into two pages</span>
</label>
<label class="option-choice">
<input type="radio" name="mode" value="stitch">
<span>Stitch split columns on one page</span>
</label>
<!-- Stitch Sub-options -->
<div class="sub-options" id="stitch-options" style="display: none;">
<span class="option-label">Stitch Direction</span>
<label class="option-choice">
<input type="radio" name="stitch_direction" value="horizontal" checked>
<span>Horizontal</span>
</label>
<label class="option-choice">
<input type="radio" name="stitch_direction" value="vertical">
<span>Vertical</span>
</label>
</div>
</div>
<!-- Add Space Option -->
<div class="option-group">
<label class="option-choice">
<input type="checkbox" id="add_space" name="add_space" checked>
<span>Add space for notes</span>
</label>
</div>
<!-- Background Color -->
<div class="option-group">
<span class="option-label">Background Color</span>
<div class="color-palette">
<div class="color-swatch" data-color="#FDF5E6" style="background-color: #FDF5E6;" title="Cream"></div>
<div class="color-swatch" data-color="#FFFFFF" style="background-color: #FFFFFF;" title="White"></div>
<div class="color-swatch" data-color="#F5F5F5" style="background-color: #F5F5F5;" title="Light Gray"></div>
<div class="color-swatch" data-color="#E6F7FF" style="background-color: #E6F7FF;" title="Light Blue"></div>
<div class="color-swatch" data-color="#FFF9E6" style="background-color: #FFF9E6;" title="Light Yellow"></div>
<button type="button" class="btn btn-outline-light color-custom-btn" data-bs-toggle="modal" data-bs-target="#colorPickerModal">
<i class="bi bi-palette me-1"></i>Custom
</button>
</div>
<input type="hidden" name="bg_color" id="bg_color" value="#FDF5E6">
</div>
<!-- Background Pattern -->
<div class="option-group">
<span class="option-label">Background Pattern</span>
<div class="pattern-choices">
<label class="pattern-choice active">
<input type="radio" name="pattern" value="" checked>
<div class="pattern-icon"></div>
<div class="pattern-name">None</div>
</label>
<label class="pattern-choice">
<input type="radio" name="pattern" value="grid">
<div class="pattern-icon"></div>
<div class="pattern-name">Grid</div>
</label>
<label class="pattern-choice">
<input type="radio" name="pattern" value="dots">
<div class="pattern-icon"></div>
<div class="pattern-name">Dots</div>
</label>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="submit-section">
<button type="submit" class="btn btn-primary submit-btn" id="submit-btn" disabled>
<i class="bi bi-check-lg me-2"></i>Resize PDF
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Color Picker Modal -->
<div class="modal fade color-picker-modal" id="colorPickerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-white">
<div class="modal-header border-secondary">
<h5 class="modal-title">
<i class="bi bi-palette me-2"></i>Choose Color
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="color-picker-wrapper" id="color-picker-ui">
<!-- Saturation/Brightness Area -->
<div class="saturation-brightness-area" id="sb-area">
<div class="gradient-white"></div>
<div class="gradient-black"></div>
<div class="sb-cursor" id="sb-cursor"></div>
</div>
<!-- Hue Slider -->
<div class="hue-slider-area" id="hue-slider">
<div class="hue-cursor" id="hue-cursor"></div>
</div>
<!-- Preview & Hex Input -->
<div class="color-preview-row">
<div class="color-preview-box" id="color-preview"></div>
<input type="text" class="form-control color-hex-input" id="color-hex-input" value="#FF0000" maxlength="7">
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="save-color-btn">
<i class="bi bi-check-lg me-1"></i>Apply Color
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', () => {
// === ELEMENTS ===
const fileList = document.getElementById('file-list');
const inputPdf = document.getElementById('input_pdf');
const outputPdf = document.getElementById('output_pdf');
const submitBtn = document.getElementById('submit-btn');
const selectedIndicator = document.getElementById('selected-indicator');
const selectedFilename = document.getElementById('selected-filename');
const clearSelection = document.getElementById('clear-selection');
const stitchOptions = document.getElementById('stitch-options');
const addSpaceCheckbox = document.getElementById('add_space');
const bgColorInput = document.getElementById('bg_color');
let selectedRow = null;
// === FILE SELECTION ===
function selectFile(filename, element) {
// Remove previous selection
if (selectedRow) {
selectedRow.classList.remove('selected');
}
// Set new selection
selectedRow = element;
selectedRow.classList.add('selected');
inputPdf.value = filename;
// Show indicator
selectedFilename.textContent = filename;
selectedIndicator.style.display = 'flex';
// Enable submit
submitBtn.disabled = false;
// Update output filename
updateOutputFilename();
}
function clearFileSelection() {
if (selectedRow) {
selectedRow.classList.remove('selected');
selectedRow = null;
}
inputPdf.value = '';
outputPdf.value = '';
selectedIndicator.style.display = 'none';
submitBtn.disabled = true;
}
// File list click handler
fileList.addEventListener('click', (e) => {
const fileItem = e.target.closest('.file-item');
if (!fileItem) return;
if (fileItem.classList.contains('folder')) {
// Navigate to folder
const path = fileItem.dataset.path;
window.location.href = `/resize/browse/${path}`;
} else if (fileItem.classList.contains('pdf')) {
// Select PDF
selectFile(fileItem.dataset.filename, fileItem);
}
});
// Clear selection button
clearSelection.addEventListener('click', clearFileSelection);
// === OUTPUT FILENAME GENERATION ===
function updateOutputFilename() {
if (!inputPdf.value) return;
const baseName = inputPdf.value.replace(/\.pdf$/i, '');
const mode = document.querySelector('input[name="mode"]:checked').value;
const addSpace = addSpaceCheckbox.checked;
let suffix = '_' + mode;
if (mode === 'stitch') {
const direction = document.querySelector('input[name="stitch_direction"]:checked').value;
suffix += `_${direction.substring(0, 4)}`;
}
if (addSpace) {
suffix += '_notes';
}
outputPdf.value = `${baseName}${suffix}.pdf`;
}
// === MODE SWITCHING ===
document.querySelectorAll('input[name="mode"]').forEach(radio => {
radio.addEventListener('change', () => {
const isStitch = radio.value === 'stitch' && radio.checked;
stitchOptions.style.display = isStitch ? 'block' : 'none';
updateOutputFilename();
});
});
document.querySelectorAll('input[name="stitch_direction"]').forEach(radio => {
radio.addEventListener('change', updateOutputFilename);
});
addSpaceCheckbox.addEventListener('change', updateOutputFilename);
// === PATTERN SELECTION ===
document.querySelectorAll('.pattern-choice').forEach(choice => {
choice.addEventListener('click', () => {
document.querySelectorAll('.pattern-choice').forEach(c => c.classList.remove('active'));
choice.classList.add('active');
});
});
// === COLOR PALETTE ===
const colorSwatches = document.querySelectorAll('.color-swatch');
function setActiveColor(color) {
bgColorInput.value = color;
localStorage.setItem('resizeBgColor', color);
colorSwatches.forEach(swatch => {
swatch.classList.toggle('active', swatch.dataset.color.toUpperCase() === color.toUpperCase());
});
}
colorSwatches.forEach(swatch => {
swatch.addEventListener('click', () => {
setActiveColor(swatch.dataset.color);
});
});
// Load saved color
const savedColor = localStorage.getItem('resizeBgColor') || '#FDF5E6';
setActiveColor(savedColor);
// === COLOR PICKER ===
const sbArea = document.getElementById('sb-area');
const sbCursor = document.getElementById('sb-cursor');
const hueSlider = document.getElementById('hue-slider');
const hueCursor = document.getElementById('hue-cursor');
const colorPreview = document.getElementById('color-preview');
const colorHexInput = document.getElementById('color-hex-input');
const saveColorBtn = document.getElementById('save-color-btn');
let hsv = { h: 0, s: 1, v: 1 };
function hsvToRgb(h, s, v) {
let r, g, b;
const i = Math.floor(h / 60);
const f = h / 60 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('').toUpperCase();
}
function hexToHsv(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return { h: 0, s: 1, v: 1 };
let r = parseInt(result[1], 16) / 255;
let g = parseInt(result[2], 16) / 255;
let b = parseInt(result[3], 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
const s = max === 0 ? 0 : d / max;
const v = max;
if (max !== min) {
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { h: h * 360, s, v };
}
function updateColorPicker() {
const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v);
const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
// Update SB area background
sbArea.style.backgroundColor = `hsl(${hsv.h}, 100%, 50%)`;
// Update cursors
sbCursor.style.left = `${hsv.s * 100}%`;
sbCursor.style.top = `${(1 - hsv.v) * 100}%`;
hueCursor.style.left = `${(hsv.h / 360) * 100}%`;
// Update preview and input
colorPreview.style.backgroundColor = hex;
colorHexInput.value = hex;
}
// Pointer handlers for touch support
function handleSBInteraction(e) {
const rect = sbArea.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
hsv.s = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
hsv.v = Math.max(0, Math.min(1, 1 - (clientY - rect.top) / rect.height));
updateColorPicker();
}
function handleHueInteraction(e) {
const rect = hueSlider.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
hsv.h = Math.max(0, Math.min(360, ((clientX - rect.left) / rect.width) * 360));
updateColorPicker();
}
// SB Area events
let sbDragging = false;
sbArea.addEventListener('mousedown', (e) => {
sbDragging = true;
handleSBInteraction(e);
});
sbArea.addEventListener('touchstart', (e) => {
sbDragging = true;
handleSBInteraction(e);
e.preventDefault();
}, { passive: false });
document.addEventListener('mousemove', (e) => {
if (sbDragging) handleSBInteraction(e);
});
document.addEventListener('touchmove', (e) => {
if (sbDragging) {
handleSBInteraction(e);
e.preventDefault();
}
}, { passive: false });
document.addEventListener('mouseup', () => sbDragging = false);
document.addEventListener('touchend', () => sbDragging = false);
// Hue Slider events
let hueDragging = false;
hueSlider.addEventListener('mousedown', (e) => {
hueDragging = true;
handleHueInteraction(e);
});
hueSlider.addEventListener('touchstart', (e) => {
hueDragging = true;
handleHueInteraction(e);
e.preventDefault();
}, { passive: false });
document.addEventListener('mousemove', (e) => {
if (hueDragging) handleHueInteraction(e);
});
document.addEventListener('touchmove', (e) => {
if (hueDragging) {
handleHueInteraction(e);
e.preventDefault();
}
}, { passive: false });
document.addEventListener('mouseup', () => hueDragging = false);
document.addEventListener('touchend', () => hueDragging = false);
// Hex input
colorHexInput.addEventListener('change', () => {
const value = colorHexInput.value.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
hsv = hexToHsv(value);
updateColorPicker();
}
});
// Save color button
saveColorBtn.addEventListener('click', () => {
setActiveColor(colorHexInput.value);
bootstrap.Modal.getInstance(document.getElementById('colorPickerModal')).hide();
});
// Initialize color picker when modal opens
document.getElementById('colorPickerModal').addEventListener('show.bs.modal', () => {
hsv = hexToHsv(bgColorInput.value || '#FDF5E6');
updateColorPicker();
});
// Initialize
updateColorPicker();
});
</script>
{% endblock %}