This commit is contained in:
phamhungd 2025-11-28 14:25:53 +07:00
parent f3f69baec1
commit 2739e82db4
5 changed files with 1643 additions and 14 deletions

BIN
.DS_Store vendored

Binary file not shown.

1393
index.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -75,7 +75,10 @@ document.addEventListener('DOMContentLoaded', () => {
const promptInput = document.getElementById('prompt');
const promptNoteInput = document.getElementById('prompt-note');
const promptHighlight = document.getElementById('prompt-highlight');
const noteHighlight = document.getElementById('note-highlight');
const themeOptionsContainer = document.getElementById('theme-options');
const promptPlaceholderText = promptInput?.getAttribute('placeholder') || '';
const promptNotePlaceholderText = promptNoteInput?.getAttribute('placeholder') || '';
const aspectRatioInput = document.getElementById('aspect-ratio');
const resolutionInput = document.getElementById('resolution');
const apiKeyInput = document.getElementById('api-key');
@ -142,10 +145,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (settings.note) promptNoteInput.value = settings.note;
if (settings.aspectRatio) aspectRatioInput.value = settings.aspectRatio;
if (settings.resolution) resolutionInput.value = settings.resolution;
if (settings.model && apiModelSelect) {
apiModelSelect.value = settings.model;
if (apiModelSelect) {
apiModelSelect.value = settings.model || apiModelSelect.value || 'gemini-3-pro-image-preview';
toggleResolutionVisibility();
}
currentTheme = settings.theme || DEFAULT_THEME;
return settings;
}
} catch (e) {
@ -155,8 +159,10 @@ document.addEventListener('DOMContentLoaded', () => {
}
function persistSettings() {
// Check if slotManager is initialized
const referenceImages = (typeof slotManager !== 'undefined') ? slotManager.getImages() : [];
// Safely collect cached reference images for restoration
const referenceImages = (typeof slotManager !== 'undefined' && typeof slotManager.serializeReferenceImages === 'function')
? slotManager.serializeReferenceImages()
: [];
const settings = {
apiKey: apiKeyInput.value,
@ -165,7 +171,8 @@ document.addEventListener('DOMContentLoaded', () => {
aspectRatio: aspectRatioInput.value,
resolution: resolutionInput.value,
model: apiModelSelect ? apiModelSelect.value : 'gemini-3-pro-image-preview',
referenceImages: referenceImages,
referenceImages,
theme: currentTheme || DEFAULT_THEME,
};
try {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
@ -208,6 +215,22 @@ document.addEventListener('DOMContentLoaded', () => {
'#a855f7', // purple
];
const DEFAULT_THEME = 'theme-sdvn';
const themeOptionsData = [
{ id: 'theme-sdvn', name: 'SDVN', gradient: 'linear-gradient(to bottom, #5858e6, #151523)' },
{ id: 'theme-vietnam', name: 'Vietnam', gradient: 'radial-gradient(ellipse at bottom, #c62921, #a21a14)' },
{ id: 'theme-skyline', name: 'Skyline', gradient: 'linear-gradient(to left, #6FB1FC, #4364F7, #0052D4)' },
{ id: 'theme-hidden-jaguar', name: 'Hidden Jaguar', gradient: 'linear-gradient(to bottom, #0fd850 0%, #f9f047 100%)' },
{ id: 'theme-wide-matrix', name: 'Wide Matrix', gradient: 'linear-gradient(to top, #fcc5e4 0%, #fda34b 15%, #ff7882 35%, #c8699e 52%, #7046aa 71%, #0c1db8 87%, #020f75 100%)' },
{ id: 'theme-rainbow', name: 'Rainbow', gradient: 'linear-gradient(to right, #0575E6, #00F260)' },
{ id: 'theme-soundcloud', name: 'SoundCloud', gradient: 'linear-gradient(to right, #f83600, #fe8c00)' },
{ id: 'theme-amin', name: 'Amin', gradient: 'linear-gradient(to right, #4A00E0, #8E2DE2)' },
];
let currentPlaceholderSegments = [];
let currentTheme = DEFAULT_THEME;
function escapeHtml(value) {
return value.replace(/[&<>"']/g, (char) => {
switch (char) {
@ -224,6 +247,7 @@ document.addEventListener('DOMContentLoaded', () => {
function buildPromptHighlightHtml(value) {
if (!promptHighlight) return '';
if (!value) {
currentPlaceholderSegments = [];
return `<span class="prompt-placeholder">${escapeHtml(promptPlaceholderText)}</span>`;
}
@ -232,24 +256,100 @@ document.addEventListener('DOMContentLoaded', () => {
let match;
let colorIndex = 0;
let html = '';
const segments = [];
while ((match = placeholderRegex.exec(value)) !== null) {
html += escapeHtml(value.slice(lastIndex, match.index));
const color = promptHighlightColors[colorIndex % promptHighlightColors.length];
segments.push({
text: match[0],
color,
});
html += `<span class="prompt-highlight-segment" style="color:${color}">${escapeHtml(match[0])}</span>`;
lastIndex = match.index + match[0].length;
colorIndex++;
}
html += escapeHtml(value.slice(lastIndex));
currentPlaceholderSegments = segments;
return html || `<span class="prompt-placeholder">${escapeHtml(promptPlaceholderText)}</span>`;
}
function buildNoteHighlightHtml(value) {
if (!noteHighlight) return '';
if (!value) {
return `<span class="note-placeholder">${escapeHtml(promptNotePlaceholderText)}</span>`;
}
const lines = value.split('\n');
return lines
.map((line, index) => {
const color = currentPlaceholderSegments[index]?.color;
const styleAttr = color ? ` style="color:${color}"` : '';
return `<span class="note-line"${styleAttr}>${escapeHtml(line)}</span>`;
})
.join('<br>');
}
function refreshPromptHighlight() {
if (!promptHighlight || !promptInput) return;
promptHighlight.innerHTML = buildPromptHighlightHtml(promptInput.value);
promptHighlight.scrollTop = promptInput.scrollTop;
promptHighlight.scrollLeft = promptInput.scrollLeft;
refreshNoteHighlight();
}
function refreshNoteHighlight() {
if (!noteHighlight || !promptNoteInput) return;
noteHighlight.innerHTML = buildNoteHighlightHtml(promptNoteInput.value);
noteHighlight.scrollTop = promptNoteInput.scrollTop;
noteHighlight.scrollLeft = promptNoteInput.scrollLeft;
}
function updateThemeSelectionUi() {
if (!themeOptionsContainer) return;
themeOptionsContainer.querySelectorAll('.theme-option').forEach(btn => {
const isActive = btn.dataset.themeId === currentTheme;
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-selected', isActive);
});
}
function applyThemeClass(themeId) {
const themeIds = themeOptionsData.map(option => option.id);
document.body.classList.remove(...themeIds);
if (themeId && themeIds.includes(themeId)) {
document.body.classList.add(themeId);
currentTheme = themeId;
} else {
currentTheme = '';
}
updateThemeSelectionUi();
}
function selectTheme(themeId, { persist = true } = {}) {
applyThemeClass(themeId);
if (persist) {
persistSettings();
}
}
function renderThemeOptions(initialTheme) {
if (!themeOptionsContainer) return;
themeOptionsContainer.innerHTML = '';
themeOptionsData.forEach(option => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'theme-option';
btn.dataset.themeId = option.id;
btn.setAttribute('role', 'option');
btn.setAttribute('aria-label', `Chọn theme ${option.name}`);
btn.style.backgroundImage = option.gradient;
btn.innerHTML = `<span class="theme-option-name">${option.name}</span>`;
btn.addEventListener('click', () => selectTheme(option.id));
themeOptionsContainer.appendChild(btn);
});
applyThemeClass(initialTheme);
}
// --- End Helper Functions ---
@ -326,6 +426,8 @@ document.addEventListener('DOMContentLoaded', () => {
});
const savedSettings = loadSettings();
renderThemeOptions(currentTheme || DEFAULT_THEME);
applyThemeClass(currentTheme || DEFAULT_THEME);
slotManager.initialize(savedSettings.referenceImages || []);
refreshPromptHighlight();
@ -359,9 +461,18 @@ document.addEventListener('DOMContentLoaded', () => {
promptHighlight.scrollTop = promptInput.scrollTop;
promptHighlight.scrollLeft = promptInput.scrollLeft;
});
promptNoteInput.addEventListener('input', persistSettings);
promptNoteInput.addEventListener('input', () => {
refreshNoteHighlight();
persistSettings();
});
promptNoteInput.addEventListener('scroll', () => {
if (!noteHighlight) return;
noteHighlight.scrollTop = promptNoteInput.scrollTop;
noteHighlight.scrollLeft = promptNoteInput.scrollLeft;
});
aspectRatioInput.addEventListener('change', persistSettings);
resolutionInput.addEventListener('change', persistSettings);
window.addEventListener('beforeunload', persistSettings);
const queueCounter = document.getElementById('queue-counter');
const queueCountText = document.getElementById('queue-count-text');

View file

@ -1,7 +1,7 @@
:root {
--bd-bg: radial-gradient(circle at 15% 20%, #2e2ce0 0%, #0b0b1b 35%, #03030b 100%);
--panel-bg: rgba(10, 11, 22, 0.95);
--panel-backdrop: rgba(6, 7, 20, 0.7);
--panel-backdrop: rgba(6, 7, 20, 0.5);
--border-color: rgba(255, 255, 255, 0.12);
--text-primary: #f5f5f5;
--text-secondary: #cbd5f5;
@ -402,6 +402,16 @@ textarea {
min-height: 100px;
}
/* Theme overrides driven from index.css gradients */
body.theme-sdvn { --bd-bg: radial-gradient(circle at 15% 20%, #2e2ce0 0%, #0b0b1b 35%, #03030b 100%); }
body.theme-vietnam { --bd-bg: radial-gradient(ellipse at bottom, #c62921, #a21a14); }
body.theme-skyline { --bd-bg: linear-gradient(to left, #6FB1FC, #4364F7, #0052D4); }
body.theme-hidden-jaguar { --bd-bg: linear-gradient(to bottom, #0fd850 0%, #f9f047 100%); }
body.theme-wide-matrix { --bd-bg: linear-gradient(to top, #fcc5e4 0%, #fda34b 15%, #ff7882 35%, #c8699e 52%, #7046aa 71%, #0c1db8 87%, #020f75 100%); }
body.theme-rainbow { --bd-bg: linear-gradient(to right, #0575E6, #00F260); }
body.theme-soundcloud { --bd-bg: linear-gradient(to right, #f83600, #fe8c00); }
body.theme-amin { --bd-bg: linear-gradient(to right, #4A00E0, #8E2DE2); }
.prompt-wrapper {
position: relative;
width: 100%;
@ -414,7 +424,7 @@ textarea {
.prompt-wrapper.prompt-highlighting {
position: relative;
border: 1px solid var(--border-color);
/* border-radius: 0.5rem; */
border-radius: 0.5rem;
/* background-color: var(--input-bg); */
backdrop-filter: blur(6px);
overflow: hidden;
@ -467,10 +477,118 @@ textarea {
font-size: 0.875rem;
line-height: 1.4;
resize: vertical;
min-height: 100px;
width: 100%;
height: 100%;
opacity:0.3
}
.note-wrapper.note-highlighting {
position: relative;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
/* background-color: var(--input-bg); */
backdrop-filter: blur(6px);
}
.note-highlight {
position: absolute;
inset: 0;
padding: 0.75rem;
overflow: auto;
pointer-events: none;
white-space: pre-wrap;
word-break: break-word;
z-index: 1;
color: var(--text-secondary);
font-family: inherit;
font-size: 0.875rem;
line-height: 1.4;
}
.note-highlight::-webkit-scrollbar {
width: 0;
height: 0;
}
.note-highlight .note-placeholder {
color: var(--text-secondary);
}
.note-highlight .note-line {
display: inline;
}
.note-wrapper.note-highlighting textarea {
position: relative;
z-index: 2;
border: none;
outline: none;
background: transparent;
color: transparent;
caret-color: var(--accent-color);
padding: 0.75rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.4;
resize: vertical;
opacity: 0.3;
width: 100%;
height: 100%;
}
.theme-option-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 0.4rem;
}
.theme-option {
position: relative;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 1rem;
padding: 0.6rem;
color: #fff;
cursor: pointer;
transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease, background-position 0.4s ease;
text-align: left;
height: 40px;
overflow: hidden;
}
.theme-option::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to top right, rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.05));
pointer-events: none;
}
.theme-option .theme-option-name {
position: relative;
z-index: 1;
font-weight: 700;
letter-spacing: 0.3px;
font-size: 0.85rem;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
}
.theme-option:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
.theme-option:hover {
transform: translateY(-1px);
border-color: var(--accent-color);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.45);
background-position: 80% 20%;
}
.theme-option.active {
border-color: var(--accent-color);
box-shadow: 0 0 0 1px rgba(251, 191, 36, 0.5), 0 12px 28px rgba(251, 191, 36, 0.18);
}
.prompt-actions {
display: flex;
justify-content: flex-end;
@ -709,7 +827,7 @@ button#generate-btn:disabled {
padding: 2rem;
overflow: hidden;
position: relative;
background: radial-gradient(circle at top, rgba(27, 38, 102, 0.4), rgba(6, 6, 18, 0.95));
background: var(--panel-backdrop);
border-radius: 1.5rem;
cursor: grab;
user-select: none;
@ -930,7 +1048,7 @@ button#generate-btn:disabled {
height: 180px;
flex-shrink: 0;
width: 100%;
background: transparent;
background: var(--panel-backdrop);
}
.history-section h3 {

View file

@ -90,8 +90,11 @@
<div class="input-group">
<label for="prompt-note">Note (sẽ được thêm vào prompt)</label>
<textarea id="prompt-note" placeholder="Thêm chi tiết hoặc điều chỉnh cho prompt..."
rows="2"></textarea>
<div class="note-wrapper note-highlighting">
<div id="note-highlight" class="note-highlight" aria-hidden="true"></div>
<textarea id="prompt-note" placeholder="Thêm chi tiết hoặc điều chỉnh cho prompt..."
rows="2"></textarea>
</div>
</div>
<div class="input-group image-inputs">
@ -381,7 +384,7 @@
aria-labelledby="api-settings-title">
<div class="popup-card" style="max-width: 480px;">
<header class="popup-header">
<h2 id="api-settings-title">Thiết lập API Key</h2>
<h2 id="api-settings-title">Setting aPix</h2>
<button id="api-settings-close" type="button" class="popup-close" aria-label="Close">&times;</button>
</header>
<div class="popup-body api-settings-body">
@ -421,6 +424,10 @@
</select>
</div>
</div>
<div class="input-group api-settings-input-group">
<label for="theme-options">Theme</label>
<div id="theme-options" class="theme-option-grid" role="listbox" aria-label="Chọn theme"></div>
</div>
<div class="controls-footer" style="justify-content: flex-end; margin-top: 0.5rem;">
<button id="save-api-settings-btn">
<span>Đóng</span>