|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Image generator</title> |
|
<style> |
|
body { |
|
font-family: Arial, sans-serif; |
|
margin: 0; |
|
padding: 0; |
|
background-color: #f9f9f9; |
|
display: flex; |
|
flex-direction: column; |
|
height: 100vh; |
|
overflow: hidden; |
|
} |
|
|
|
.header { |
|
background-color: #007bff; |
|
color: white; |
|
padding: 15px; |
|
text-align: center; |
|
font-size: 24px; |
|
position: sticky; |
|
top: 0; |
|
z-index: 2; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.header img { |
|
max-height: 50px; |
|
margin-right: 10px; |
|
cursor: pointer; |
|
} |
|
|
|
.header img:hover + .tooltip { |
|
display: block; |
|
} |
|
|
|
.tooltip { |
|
display: none; |
|
position: absolute; |
|
color: white; |
|
background-color: rgba(0, 0, 0, 0.7); |
|
padding: 5px; |
|
border-radius: 5px; |
|
font-size: 14px; |
|
margin-left: -30px; |
|
margin-top: -30px; |
|
} |
|
|
|
.container { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
flex: 1; |
|
overflow-y: auto; |
|
} |
|
|
|
.api-info { |
|
margin: 20px; |
|
text-align: center; |
|
font-size: 16px; |
|
color: #333; |
|
} |
|
|
|
.api-info a { |
|
color: #007bff; |
|
text-decoration: none; |
|
} |
|
|
|
.api-info a:hover { |
|
text-decoration: underline; |
|
} |
|
|
|
.controls { |
|
width: 80%; |
|
margin: 20px auto; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: stretch; |
|
} |
|
|
|
.controls label { |
|
margin-bottom: 5px; |
|
font-size: 14px; |
|
color: #333; |
|
} |
|
|
|
.controls input[type="password"], |
|
.controls input[type="text"], |
|
.controls textarea { |
|
width: 100%; |
|
padding: 10px; |
|
margin-bottom: 10px; |
|
border: 1px solid #ccc; |
|
border-radius: 5px; |
|
font-size: 14px; |
|
box-sizing: border-box; |
|
transition: border-color 0.3s; |
|
} |
|
|
|
.controls input[type="password"]:focus, |
|
.controls input[type="text"]:focus, |
|
.controls textarea:focus { |
|
border-color: #007bff; |
|
} |
|
|
|
.controls button { |
|
padding: 10px; |
|
background-color: #007bff; |
|
color: white; |
|
border: none; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
font-size: 16px; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
.controls button:hover { |
|
background-color: #0056b3; |
|
} |
|
|
|
.sliders { |
|
display: flex; |
|
flex-direction: column; |
|
margin: 10px 0; |
|
} |
|
|
|
.slider-container { |
|
display: flex; |
|
align-items: center; |
|
margin-bottom: 15px; |
|
flex-wrap: nowrap; |
|
} |
|
|
|
.slider-container label { |
|
margin-right: 10px; |
|
font-size: 14px; |
|
color: #333; |
|
width: 100px; |
|
} |
|
|
|
.slider-value { |
|
width: 40px; |
|
text-align: center; |
|
font-size: 14px; |
|
margin-left: 10px; |
|
} |
|
|
|
.output { |
|
width: 80%; |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); |
|
gap: 20px; |
|
margin: 20px auto; |
|
} |
|
|
|
.output img { |
|
width: 100%; |
|
height: auto; |
|
border-radius: 10px; |
|
cursor: pointer; |
|
max-height: 620px; |
|
object-fit: contain; |
|
transition: transform 0.3s; |
|
} |
|
|
|
.output img:hover { |
|
transform: scale(1.05); |
|
} |
|
|
|
.image-container { |
|
position: relative; |
|
} |
|
|
|
.delete-button { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
background-color: red; |
|
color: white; |
|
border: none; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
padding: 5px 10px; |
|
font-size: 12px; |
|
display: none; |
|
} |
|
|
|
.image-container:hover .delete-button { |
|
display: block; |
|
} |
|
|
|
#status { |
|
text-align: center; |
|
color: #333; |
|
margin-top: 10px; |
|
font-size: 14px; |
|
} |
|
|
|
.modal { |
|
display: none; |
|
position: fixed; |
|
z-index: 1000; |
|
left: 0; |
|
top: 0; |
|
width: 100%; |
|
height: 100%; |
|
overflow: auto; |
|
background-color: rgba(0, 0, 0, 0.8); |
|
justify-content: center; |
|
align-items: center; |
|
padding: 20px; |
|
} |
|
|
|
.modal-content { |
|
background-color: white; |
|
padding: 50px; |
|
border-radius: 10px; |
|
max-width: 800px; |
|
width: 100%; |
|
display: flex; |
|
flex-direction: row; |
|
align-items: center; |
|
} |
|
|
|
.modal img { |
|
max-width: 50%; |
|
height: auto; |
|
border-radius: 10px; |
|
margin-right: 20px; |
|
object-fit: contain; |
|
} |
|
|
|
.prompt-info { |
|
margin-top: 0px; |
|
font-size: 14px; |
|
max-width: 300px; |
|
} |
|
|
|
.copy-prompt { |
|
background-color: #007bff; |
|
color: white; |
|
border: none; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
padding: 5px 10px; |
|
margin-top: 10px; |
|
} |
|
|
|
.copy-prompt:hover { |
|
background-color: #0056b3; |
|
} |
|
|
|
@media (max-width: 600px) { |
|
.slider-container label { |
|
width: auto; |
|
} |
|
|
|
.slider-container { |
|
flex-direction: column; |
|
align-items: stretch; |
|
} |
|
|
|
.slider-value { |
|
margin-left: 0; |
|
margin-top: 5px; |
|
} |
|
|
|
.modal img { |
|
max-width: 100%; |
|
} |
|
} |
|
|
|
input[type="range"] { |
|
width: 100%; |
|
} |
|
|
|
.toggle-button { |
|
background-color: #007bff; |
|
color: white; |
|
border: none; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
padding: 5px 10px; |
|
margin-left: 10px; |
|
font-size: 14px; |
|
} |
|
|
|
.toggle-button:hover { |
|
background-color: #0056b3; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<div class="header"> |
|
<img src="palestine.png" alt="Logo" id="logo"> |
|
<span class="tooltip">Free Palestine</span> |
|
Pray for Palestine |
|
</div> |
|
|
|
<div class="api-info"> |
|
<p>Get the API key and 50 free credits to generate images and purchase from here<a href="https://api.bfl.ml/auth/login#" target="_blank">BFL API Portal</a>.</p> |
|
</div> |
|
|
|
<div class="container"> |
|
<div class="controls"> |
|
<label for="api-key">API Key:</label> |
|
<div style="display: flex; align-items: center;"> |
|
<input type="password" id="api-key" placeholder="Enter your API Key"> |
|
<button class="toggle-button" id="toggle-api-key">Show</button> |
|
</div> |
|
|
|
<label for="prompt">Image Description:</label> |
|
<textarea id="prompt" placeholder="Describe the image you want to generate" rows="4" maxlength="184"></textarea> |
|
|
|
<div class="sliders"> |
|
<div class="slider-container"> |
|
<label for="width">Width:</label> |
|
<input type="range" id="width" min="512" max="1440" step="64" value="1024"> |
|
<input class="slider-value" type="text" id="width-value" value="1024" readonly> |
|
</div> |
|
|
|
<div class="slider-container"> |
|
<label for="height">Height:</label> |
|
<input type="range" id="height" min="512" max="1440" step="64" value="768"> |
|
<input class="slider-value" type="text" id="height-value" value="768" readonly> |
|
</div> |
|
|
|
<div class="slider-container"> |
|
<label for="steps">Steps:</label> |
|
<input type="range" id="steps" min="1" max="100" step="1" value="8"> |
|
<input class="slider-value" type="text" id="steps-value" value="8" readonly> |
|
</div> |
|
</div> |
|
|
|
<button id="generate-btn">Generate Image</button> |
|
<p id="status"></p> |
|
</div> |
|
|
|
<div class="output" id="output-container"></div> |
|
</div> |
|
|
|
<div class="modal" id="modal"> |
|
<div class="modal-content"> |
|
<img id="modal-image" alt="Enlarged Image"> |
|
<div> |
|
<div class="prompt-info" id="prompt-info"></div> |
|
<div class="settings-info" id="settings-info"></div> |
|
<button id="copy-prompt" class="copy-prompt">Copy Prompt</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
const generateBtn = document.getElementById('generate-btn'); |
|
const promptInput = document.getElementById('prompt'); |
|
const apiKeyInput = document.getElementById('api-key'); |
|
const widthSlider = document.getElementById('width'); |
|
const heightSlider = document.getElementById('height'); |
|
const stepsSlider = document.getElementById('steps'); |
|
const widthValue = document.getElementById('width-value'); |
|
const heightValue = document.getElementById('height-value'); |
|
const stepsValue = document.getElementById('steps-value'); |
|
const statusElement = document.getElementById('status'); |
|
const outputContainer = document.getElementById('output-container'); |
|
const modal = document.getElementById('modal'); |
|
const modalImage = document.getElementById('modal-image'); |
|
const promptInfo = document.getElementById('prompt-info'); |
|
const settingsInfo = document.getElementById('settings-info'); |
|
const copyPromptBtn = document.getElementById('copy-prompt'); |
|
const toggleApiKeyBtn = document.getElementById('toggle-api-key'); |
|
|
|
let isApiKeyVisible = false; |
|
|
|
window.onload = () => { |
|
const savedPrompt = localStorage.getItem('lastPrompt'); |
|
const savedApiKey = localStorage.getItem('apiKey'); |
|
if (savedPrompt) { |
|
promptInput.value = savedPrompt; |
|
} |
|
if (savedApiKey) { |
|
apiKeyInput.value = savedApiKey; |
|
} |
|
|
|
loadImagesFromStorage(); |
|
}; |
|
|
|
function loadImagesFromStorage() { |
|
const savedImages = JSON.parse(localStorage.getItem('images')) || []; |
|
savedImages.forEach(imgObj => addImageToOutput(imgObj.url, imgObj.prompt, imgObj.width, imgObj.height, imgObj.steps)); |
|
} |
|
|
|
widthSlider.addEventListener('input', () => { |
|
widthValue.value = widthSlider.value; |
|
localStorage.setItem('lastWidth', widthSlider.value); |
|
}); |
|
|
|
heightSlider.addEventListener('input', () => { |
|
heightValue.value = heightSlider.value; |
|
localStorage.setItem('lastHeight', heightSlider.value); |
|
}); |
|
|
|
stepsSlider.addEventListener('input', () => { |
|
stepsValue.value = stepsSlider.value; |
|
localStorage.setItem('lastSteps', stepsSlider.value); |
|
}); |
|
|
|
generateBtn.addEventListener('click', async () => { |
|
const prompt = promptInput.value; |
|
const apiKey = apiKeyInput.value; |
|
const width = widthSlider.value; |
|
const height = heightSlider.value; |
|
const steps = stepsSlider.value; |
|
|
|
|
|
if (prompt.length > 184) { |
|
alert('The prompt cannot exceed 184 characters.'); |
|
return; |
|
} |
|
|
|
if (!prompt || !apiKey) { |
|
alert('Please enter both a prompt and API key!'); |
|
return; |
|
} |
|
|
|
localStorage.setItem('lastPrompt', prompt); |
|
localStorage.setItem('apiKey', apiKey); |
|
|
|
statusElement.textContent = 'Generating image...'; |
|
|
|
try { |
|
const requestId = await sendImageGenerationRequest(prompt, width, height, steps, apiKey); |
|
const imageUrl = await pollResult(requestId, apiKey); |
|
addImageToOutput(imageUrl, prompt, width, height, steps); |
|
statusElement.textContent = 'Image generated successfully!'; |
|
} catch (error) { |
|
statusElement.textContent = `Error: ${error.message}`; |
|
} |
|
}); |
|
|
|
toggleApiKeyBtn.addEventListener('click', () => { |
|
isApiKeyVisible = !isApiKeyVisible; |
|
apiKeyInput.type = isApiKeyVisible ? 'text' : 'password'; |
|
toggleApiKeyBtn.textContent = isApiKeyVisible ? 'Hide' : 'Show'; |
|
}); |
|
|
|
function addImageToOutput(imageUrl, prompt, width, height, steps) { |
|
const container = document.createElement('div'); |
|
container.className = 'image-container'; |
|
|
|
const img = document.createElement('img'); |
|
img.src = imageUrl; |
|
img.alt = "Generated Image"; |
|
img.onclick = () => openModal(imageUrl, prompt, width, height, steps); |
|
|
|
const deleteButton = document.createElement('button'); |
|
deleteButton.className = 'delete-button'; |
|
deleteButton.innerText = 'Delete'; |
|
deleteButton.onclick = (e) => { |
|
e.stopPropagation(); |
|
container.remove(); |
|
removeImageFromStorage(imageUrl); |
|
}; |
|
|
|
container.appendChild(img); |
|
container.appendChild(deleteButton); |
|
outputContainer.prepend(container); |
|
|
|
let savedImages = JSON.parse(localStorage.getItem('images')) || []; |
|
if (!savedImages.some(imgObj => imgObj.url === imageUrl)) { |
|
savedImages.push({ url: imageUrl, prompt: prompt, width: width, height: height, steps: steps }); |
|
localStorage.setItem('images', JSON.stringify(savedImages)); |
|
} |
|
} |
|
|
|
function removeImageFromStorage(imageUrl) { |
|
let savedImages = JSON.parse(localStorage.getItem('images')) || []; |
|
savedImages = savedImages.filter(imgObj => imgObj.url !== imageUrl); |
|
localStorage.setItem('images', JSON.stringify(savedImages)); |
|
} |
|
|
|
function openModal(imageUrl, prompt, width, height, steps) { |
|
modal.style.display = 'flex'; |
|
modalImage.src = imageUrl; |
|
promptInfo.innerText = `Prompt: ${prompt}`; |
|
settingsInfo.innerText = `Width: ${width}, Height: ${height}, Steps: ${steps}`; |
|
} |
|
|
|
copyPromptBtn.onclick = () => { |
|
const promptText = promptInfo.innerText.replace('Prompt: ', ''); |
|
navigator.clipboard.writeText(promptText); |
|
}; |
|
|
|
modal.onclick = (event) => { |
|
if (event.target === modal) { |
|
modal.style.display = 'none'; |
|
} |
|
}; |
|
|
|
async function sendImageGenerationRequest(prompt, width, height, steps, apiKey) { |
|
const response = await fetch('https://api.bfl.ml/v1/image', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'x-key': apiKey, |
|
}, |
|
body: JSON.stringify({ |
|
prompt: prompt, |
|
width: parseInt(width), |
|
height: parseInt(height), |
|
steps: parseInt(steps), |
|
}), |
|
}); |
|
|
|
const data = await response.json(); |
|
return data.id; |
|
} |
|
|
|
async function pollResult(requestId, apiKey) { |
|
while (true) { |
|
const result = await getResult(requestId, apiKey); |
|
const status = result.status; |
|
|
|
if (status === 'Ready') { |
|
return result.result.sample; |
|
} else if (['Error', 'Content Moderated', 'Request Moderated', 'Task not found'].includes(status)) { |
|
throw new Error(`Image generation failed. Status: ${status}`); |
|
} |
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
} |
|
} |
|
|
|
async function getResult(requestId, apiKey) { |
|
const response = await fetch(`https://api.bfl.ml/v1/get_result?id=${requestId}`, { |
|
method: 'GET', |
|
headers: { |
|
'x-key': apiKey, |
|
}, |
|
}); |
|
|
|
const data = await response.json(); |
|
return data; |
|
} |
|
</script> |
|
|
|
</body> |
|
</html> |