beta3
This commit is contained in:
parent
f3f69baec1
commit
2739e82db4
5 changed files with 1643 additions and 14 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
123
static/script.js
123
static/script.js
|
|
@ -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');
|
||||
|
|
|
|||
128
static/style.css
128
static/style.css
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">×</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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue