This commit is contained in:
phamhungd 2025-11-22 01:33:28 +07:00
parent 8fbb90f21f
commit 461ac39b88
3 changed files with 163 additions and 87 deletions

View file

@ -8,6 +8,8 @@ const SETTINGS_STORAGE_KEY = 'gemini-image-app-settings';
const ZOOM_STEP = 0.1; const ZOOM_STEP = 0.1;
const MIN_ZOOM = 0.4; const MIN_ZOOM = 0.4;
const MAX_ZOOM = 4; const MAX_ZOOM = 4;
const SIDEBAR_MIN_WIDTH = 260;
const SIDEBAR_MAX_WIDTH = 520;
const infoContent = { const infoContent = {
title: 'Thông tin', title: 'Thông tin',
@ -47,9 +49,13 @@ const docsContent = {
], ],
}; };
const helpContent = {
title: 'Thông tin & Hướng dẫn',
sections: [...infoContent.sections, ...docsContent.sections],
};
const POPUP_CONTENT = { const POPUP_CONTENT = {
info: infoContent, help: helpContent,
docs: docsContent,
}; };
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -70,6 +76,8 @@ document.addEventListener('DOMContentLoaded', () => {
const imageInputGrid = document.getElementById('image-input-grid'); const imageInputGrid = document.getElementById('image-input-grid');
const imageDisplayArea = document.querySelector('.image-display-area'); const imageDisplayArea = document.querySelector('.image-display-area');
const canvasToolbar = document.querySelector('.canvas-toolbar'); const canvasToolbar = document.querySelector('.canvas-toolbar');
const sidebar = document.querySelector('.sidebar');
const resizeHandle = document.querySelector('.sidebar-resize-handle');
let zoomLevel = 1; let zoomLevel = 1;
let panOffset = { x: 0, y: 0 }; let panOffset = { x: 0, y: 0 };
@ -224,6 +232,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
loadGallery(); loadGallery();
setupSidebarResizer(sidebar, resizeHandle);
function setViewState(state) { function setViewState(state) {
placeholderState.classList.add('hidden'); placeholderState.classList.add('hidden');
@ -472,4 +481,48 @@ document.addEventListener('DOMContentLoaded', () => {
panOffset = { x: 0, y: 0 }; panOffset = { x: 0, y: 0 };
setImageTransform(); setImageTransform();
} }
function setupSidebarResizer(sidebar, handle) {
if (!sidebar || !handle) return;
let isResizing = false;
let activePointerId = null;
const updateWidth = (clientX) => {
const sidebarRect = sidebar.getBoundingClientRect();
let newWidth = clientX - sidebarRect.left;
newWidth = clamp(newWidth, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH);
sidebar.style.width = `${newWidth}px`;
};
const stopResize = () => {
if (!isResizing) return;
isResizing = false;
if (activePointerId !== null) {
try {
handle.releasePointerCapture(activePointerId);
} catch (error) {
console.warn('Unable to release pointer capture', error);
}
activePointerId = null;
}
document.body.style.cursor = '';
};
handle.addEventListener('pointerdown', (event) => {
isResizing = true;
activePointerId = event.pointerId;
handle.setPointerCapture(activePointerId);
document.body.style.cursor = 'ew-resize';
event.preventDefault();
});
document.addEventListener('pointermove', (event) => {
if (!isResizing) return;
updateWidth(event.clientX);
});
document.addEventListener('pointerup', stopResize);
document.addEventListener('pointercancel', stopResize);
}
}); });

View file

@ -47,7 +47,7 @@ a:hover {
display: flex; display: flex;
height: 100vh; height: 100vh;
width: 100%; width: 100%;
gap: 1rem; gap: 0;
padding: 1rem; padding: 1rem;
position: relative; position: relative;
z-index: 1; z-index: 1;
@ -69,21 +69,25 @@ a:hover {
border-radius: 1rem; border-radius: 1rem;
} }
.top-toolbar { .sidebar-resize-handle {
background: transparent; width: 4px;
border: 1px solid rgba(255, 255, 255, 0.08); flex-shrink: 0;
cursor: ew-resize;
position: relative;
display: flex; display: flex;
flex-direction: row; align-self: stretch;
justify-content: flex-end; }
align-items: center;
gap: 0.75rem; .sidebar-resize-handle::before {
padding: 1rem; content: '';
border-radius: 1rem; position: absolute;
width: 100%; inset: 0;
background: transparent;
} }
.content-area { .content-area {
flex: 1; flex: 1;
margin-left: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
@ -93,21 +97,19 @@ a:hover {
min-height: 0; min-height: 0;
} }
.toolbar-info {
display: flex;
gap: 0.5rem;
}
.toolbar-info-btn { .toolbar-info-btn {
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
color: var(--text-secondary); color: var(--text-secondary);
padding: 0.2rem 0.6rem; padding: 0.35rem 0.6rem;
border-radius: 999px; border-radius: 0;
font-size: 0.7rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s, color 0.2s, background 0.2s, box-shadow 0.2s; transition: border-color 0.2s, color 0.2s, background 0.2s, box-shadow 0.2s;
line-height: 1.2; line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
} }
.toolbar-info-btn:hover:not(:disabled) { .toolbar-info-btn:hover:not(:disabled) {
@ -122,9 +124,29 @@ a:hover {
cursor: not-allowed; cursor: not-allowed;
} }
.sidebar-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
width: 100%;
}
.info-icon-btn {
width: 36px;
height: 36px;
padding: 0;
min-width: 36px;
border-radius: 0;
font-size: 1rem;
font-weight: 600;
line-height: 1;
}
.brand { .brand {
margin-bottom: 2rem; margin-bottom: 0;
} }
.brand h1 { .brand h1 {

View file

@ -15,73 +15,74 @@
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<aside class="sidebar"> <aside class="sidebar">
<div class="brand"> <div class="sidebar-header">
<h1>aPix <span class="badge">by SDVN</span></h1> <div class="brand">
</div> <h1>aPix <span class="badge">by SDVN</span></h1>
<div class="controls-section">
<div class="controls-content">
<div class="input-group">
<label for="api-key">API Key</label>
<input type="password" id="api-key" placeholder="Google Cloud API Key">
</div> </div>
<button type="button" class="toolbar-info-btn info-icon-btn" data-popup-target="help"
<div class="input-group"> aria-label="Thông tin và hướng dẫn">
<label for="prompt">Prompt</label> <span aria-hidden="true">i</span>
<textarea id="prompt" placeholder="Describe your imagination..." rows="6"></textarea>
</div>
<div class="input-group image-inputs">
<div class="image-input-header">
<label>Reference Images (optional)</label>
<span>Drag & drop up to 16 files</span>
</div>
<div id="image-input-grid" class="image-input-grid" aria-live="polite"></div>
</div>
<div class="input-group">
<label for="aspect-ratio">Aspect Ratio</label>
<select id="aspect-ratio">
<option value="Auto">Auto (Default)</option>
<option value="1:1">1:1 (Square)</option>
<option value="16:9">16:9 (Widescreen)</option>
<option value="4:3">4:3 (Standard)</option>
<option value="3:4">3:4 (Portrait)</option>
<option value="9:16">9:16 (Mobile)</option>
<option value="2:3">2:3</option>
<option value="3:2">3:2</option>
<option value="4:5">4:5</option>
<option value="5:4">5:4</option>
<option value="21:9">21:9 (Cinema)</option>
</select>
</div>
<div class="input-group">
<label for="resolution">Resolution</label>
<select id="resolution">
<option value="1K" selected>1K</option>
<option value="2K">2K</option>
<option value="4K">4K</option>
</select>
</div>
</div>
<div class="controls-footer">
<button id="generate-btn">
<span>Generate</span>
<div class="btn-shine"></div>
</button> </button>
</div> </div>
</div>
</aside> <div class="controls-section">
<div class="content-area"> <div class="controls-content">
<div class="top-toolbar"> <div class="input-group">
<div class="toolbar-info"> <label for="api-key">API Key</label>
<button type="button" class="toolbar-info-btn" data-popup-target="docs">Docs</button> <input type="password" id="api-key" placeholder="Google Cloud API Key">
<button type="button" class="toolbar-info-btn" data-popup-target="info">Info</button> </div>
<div class="input-group">
<label for="prompt">Prompt</label>
<textarea id="prompt" placeholder="Describe your imagination..." rows="6"></textarea>
</div>
<div class="input-group image-inputs">
<div class="image-input-header">
<label>Reference Images (optional)</label>
<span>Drag & drop up to 16 files</span>
</div>
<div id="image-input-grid" class="image-input-grid" aria-live="polite"></div>
</div>
<div class="input-group">
<label for="aspect-ratio">Aspect Ratio</label>
<select id="aspect-ratio">
<option value="Auto">Auto (Default)</option>
<option value="1:1">1:1 (Square)</option>
<option value="16:9">16:9 (Widescreen)</option>
<option value="4:3">4:3 (Standard)</option>
<option value="3:4">3:4 (Portrait)</option>
<option value="9:16">9:16 (Mobile)</option>
<option value="2:3">2:3</option>
<option value="3:2">3:2</option>
<option value="4:5">4:5</option>
<option value="5:4">5:4</option>
<option value="21:9">21:9 (Cinema)</option>
</select>
</div>
<div class="input-group">
<label for="resolution">Resolution</label>
<select id="resolution">
<option value="1K" selected>1K</option>
<option value="2K">2K</option>
<option value="4K">4K</option>
</select>
</div>
</div>
<div class="controls-footer">
<button id="generate-btn">
<span>Generate</span>
<div class="btn-shine"></div>
</button>
</div>
</div> </div>
</div> </aside>
<div class="sidebar-resize-handle" aria-hidden="true"></div>
<div class="content-area">
<main class="main-content"> <main class="main-content">
<div class="image-display-area"> <div class="image-display-area">
<div id="placeholder-state" class="state-view"> <div id="placeholder-state" class="state-view">
@ -141,4 +142,4 @@
<script type="module" src="{{ url_for('static', filename='script.js') }}"></script> <script type="module" src="{{ url_for('static', filename='script.js') }}"></script>
</body> </body>
</html> </html>