Gallery / gallery.html
sugakrit6's picture
Update gallery.html
7ffc020 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gallery Pro: Grain & Notes</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<style>
:root {
--bg: #121212;
--surface: #1e1e1e;
--primary: #bb86fc;
--secondary: #03dac6;
--text: #e0e0e0;
--border: #333;
}
body {
font-family: 'Segoe UI', sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
display: flex;
height: 100vh;
overflow: hidden;
}
/* --- Sidebar --- */
.sidebar {
width: 260px;
background-color: var(--surface);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
flex-shrink: 0;
}
.sidebar h2 { color: var(--primary); margin-top: 0; }
.album-list {
list-style: none;
padding: 0;
margin-top: 20px;
flex-grow: 1;
overflow-y: auto;
}
.album-item {
padding: 12px;
cursor: pointer;
border-radius: 6px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
background: #252525;
transition: 0.2s;
}
.album-item:hover { background-color: #333; }
.album-item.active { background-color: var(--primary); color: #000; }
input, select, textarea {
background: #2b2b2b;
border: 1px solid #444;
color: white;
padding: 8px;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
}
button {
background-color: var(--primary);
border: none;
color: #000;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: 0.2s;
}
button:hover { opacity: 0.9; }
button.secondary { background-color: #444; color: white; }
button.action-btn { background-color: var(--secondary); color: #000; margin-right: 5px; }
/* --- Main Content --- */
.main {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
padding: 20px;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.top-bar { display: flex; justify-content: space-between; align-items: center; }
.action-bar { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
.notes-area {
width: 100%;
height: 60px;
margin-top: 10px;
resize: none;
font-family: monospace;
font-size: 0.9rem;
}
.gallery-container {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.img-card {
background-color: var(--surface);
border-radius: 8px;
overflow: hidden;
position: relative;
group: hover;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.img-card img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.img-actions {
position: absolute;
bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,0.8);
padding: 5px;
display: flex;
justify-content: space-around;
transform: translateY(100%);
transition: transform 0.2s;
}
.img-card:hover .img-actions { transform: translateY(0); }
.img-btn {
background: none; border: none; color: white; font-size: 1.2rem; cursor: pointer; padding: 5px;
}
.img-btn:hover { color: var(--primary); }
/* --- Editor Modal --- */
.modal {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.95);
display: none;
flex-direction: column;
z-index: 2000;
padding: 20px;
}
.modal.open { display: flex; }
.editor-workspace {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background: #000;
border: 1px solid #333;
margin-bottom: 10px;
}
.editor-workspace img { max-width: 100%; max-height: 80vh; display: block; }
.editor-toolbar {
background: var(--surface);
padding: 15px;
border-radius: 8px;
display: flex;
gap: 20px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.tool-group {
display: flex;
align-items: center;
gap: 10px;
border-right: 1px solid #444;
padding-right: 20px;
}
.tool-group:last-child { border: none; }
label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; }
</style>
</head>
<body>
<div class="sidebar">
<h2>Gallery Pro</h2>
<div style="background: #252525; padding: 10px; border-radius: 6px;">
<input type="text" id="newAlbumName" placeholder="New Album Name">
<select id="newAlbumAccess">
<option value="public">🔓 Allow All</option>
<option value="private">🔒 Limited Access</option>
</select>
<button onclick="createAlbum()" style="width:100%">+ Create</button>
</div>
<div class="album-list" id="albumList">
</div>
</div>
<div class="main">
<div class="header">
<div class="top-bar">
<h1 id="currentAlbumTitle" style="margin:0">Select Album</h1>
<span id="accessBadge" style="padding: 5px 10px; border-radius: 4px; background:#333; font-size: 0.8rem;"></span>
</div>
<textarea id="albumNotes" class="notes-area" placeholder="Write notes about this album..." oninput="saveNotes()"></textarea>
<div class="action-bar" id="actionBar" style="display:none;">
<input type="file" id="fileInput" accept="image/*" style="display:none" onchange="handleFileUpload(this)">
<button onclick="document.getElementById('fileInput').click()">📁 Upload File</button>
<button class="secondary" onclick="addFromUrl()">🌐 Add from URL</button>
<button class="action-btn" onclick="downloadAlbumZip()">💾 Download Album (.zip)</button>
</div>
</div>
<div class="gallery-container">
<div class="gallery-grid" id="galleryGrid">
<p style="opacity:0.5; margin: 20px;">Select an album to start.</p>
</div>
</div>
</div>
<div class="modal" id="editorModal">
<div class="editor-workspace">
<img id="editTarget" src="" alt="Editing...">
</div>
<div class="editor-toolbar">
<div class="tool-group">
<label>Grain</label>
<input type="range" id="grainRange" min="0" max="100" value="0">
<button class="secondary" onclick="applyGrain()">Apply</button>
</div>
<div class="tool-group">
<button class="secondary" onclick="rotateImage()">↻ Rotate 90°</button>
<button class="secondary" id="btnCrop" onclick="toggleCrop()">✂ Crop</button>
</div>
<div class="tool-group" style="border:none">
<button class="secondary" onclick="restoreOriginal()" style="background:#cf6679; color:white">Restore</button>
<button onclick="saveEdits()">✅ Save Changes</button>
<button class="secondary" onclick="closeModal()">Cancel</button>
</div>
</div>
</div>
<script>
// --- State ---
let albums = JSON.parse(localStorage.getItem('galleryPro_albums')) || [];
let currentAlbumId = null;
let cropper = null;
let currentEditIndex = -1; // Which image in the array are we editing?
let originalImgData = null; // Backup for restore
// --- Album Management ---
function renderAlbums() {
const list = document.getElementById('albumList');
list.innerHTML = '';
albums.forEach(album => {
const li = document.createElement('li');
li.className = `album-item ${currentAlbumId === album.id ? 'active' : ''}`;
li.innerHTML = `<span>${album.name} <small>(${album.images.length})</small></span> ${album.access === 'private' ? '🔒' : '🔓'}`;
li.onclick = () => selectAlbum(album.id);
list.appendChild(li);
});
}
function createAlbum() {
const name = document.getElementById('newAlbumName').value;
const access = document.getElementById('newAlbumAccess').value;
if (!name) return alert("Enter a name");
const newAlbum = { id: Date.now(), name, access, notes: "", images: [] };
albums.push(newAlbum);
saveData();
document.getElementById('newAlbumName').value = '';
renderAlbums();
selectAlbum(newAlbum.id);
}
function selectAlbum(id) {
currentAlbumId = id;
const album = albums.find(a => a.id === id);
document.getElementById('currentAlbumTitle').innerText = album.name;
document.getElementById('albumNotes').value = album.notes || "";
document.getElementById('accessBadge').innerText = album.access === 'public' ? "Public Access" : "Restricted Access";
document.getElementById('accessBadge').style.background = album.access === 'public' ? "#2e7d32" : "#c62828";
document.getElementById('actionBar').style.display = 'flex';
renderGallery();
renderAlbums(); // Update active state
}
function saveNotes() {
if (!currentAlbumId) return;
const album = albums.find(a => a.id === currentAlbumId);
album.notes = document.getElementById('albumNotes').value;
saveData();
}
// --- Gallery Rendering ---
function renderGallery() {
const album = albums.find(a => a.id === currentAlbumId);
const grid = document.getElementById('galleryGrid');
grid.innerHTML = '';
if (album.images.length === 0) {
grid.innerHTML = '<p style="opacity:0.5; width:100%">No images. Upload or paste a URL.</p>';
return;
}
album.images.forEach((img, index) => {
const card = document.createElement('div');
card.className = 'img-card';
card.innerHTML = `
<img src="${img.data}" alt="Gallery Image">
<div class="img-actions">
<button class="img-btn" onclick="openEditor(${index})" title="Edit">✏️</button>
<button class="img-btn" onclick="downloadSingle(${index})" title="Download">⬇️</button>
<button class="img-btn" onclick="deleteImage(${index})" title="Delete" style="color:#cf6679">🗑️</button>
</div>
`;
grid.appendChild(card);
});
}
// --- Adding Images ---
function handleFileUpload(input) {
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = (e) => addToAlbum(e.target.result);
reader.readAsDataURL(input.files[0]);
}
}
function addFromUrl() {
const url = prompt("Paste Image URL:");
if (url) {
// We attempt to draw it to canvas to bypass basic hotlinking limits,
// but CORS might block it. If it blocks, we save the URL string directly.
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
try {
addToAlbum(canvas.toDataURL("image/jpeg"));
} catch (e) {
// Tainted canvas fallback
addToAlbum(url);
}
};
img.onerror = () => addToAlbum(url); // Fallback if CORS fails completely
img.src = url;
}
}
function addToAlbum(dataString) {
const album = albums.find(a => a.id === currentAlbumId);
album.images.push({ data: dataString, date: new Date().toISOString() });
saveData();
renderGallery();
renderAlbums();
}
// --- Editor Logic ---
function openEditor(index) {
const album = albums.find(a => a.id === currentAlbumId);
currentEditIndex = index;
const imgData = album.images[index].data;
originalImgData = imgData; // Save for restore
const imgEl = document.getElementById('editTarget');
imgEl.src = imgData;
document.getElementById('editorModal').classList.add('open');
// Wait for image to load before allowing crop
imgEl.onload = () => {
if (cropper) cropper.destroy();
};
}
function toggleCrop() {
const imgEl = document.getElementById('editTarget');
if (cropper) {
// Apply crop
const canvas = cropper.getCroppedCanvas();
imgEl.src = canvas.toDataURL();
cropper.destroy();
cropper = null;
document.getElementById('btnCrop').innerText = "✂ Crop";
document.getElementById('btnCrop').classList.add('secondary');
} else {
// Start crop
cropper = new Cropper(imgEl, { viewMode: 1 });
document.getElementById('btnCrop').innerText = "Apply Crop";
document.getElementById('btnCrop').classList.remove('secondary');
}
}
function rotateImage() {
if (cropper) cropper.rotate(90);
else {
// Manual canvas rotation if cropper isn't active
const img = document.getElementById('editTarget');
const canvas = document.createElement('canvas');
canvas.width = img.naturalHeight;
canvas.height = img.naturalWidth;
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width/2, canvas.height/2);
ctx.rotate(90 * Math.PI / 180);
ctx.drawImage(img, -img.naturalWidth/2, -img.naturalHeight/2);
img.src = canvas.toDataURL();
}
}
function applyGrain() {
const amount = parseInt(document.getElementById('grainRange').value);
if(amount === 0) return;
const img = document.getElementById('editTarget');
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const noise = (Math.random() - 0.5) * amount * 2;
data[i] += noise;
data[i+1] += noise;
data[i+2] += noise;
}
ctx.putImageData(imageData, 0, 0);
img.src = canvas.toDataURL();
document.getElementById('grainRange').value = 0; // Reset slider
}
function restoreOriginal() {
if (cropper) cropper.destroy();
cropper = null;
document.getElementById('editTarget').src = originalImgData;
}
function saveEdits() {
if (cropper) {
const canvas = cropper.getCroppedCanvas();
document.getElementById('editTarget').src = canvas.toDataURL();
cropper.destroy();
cropper = null;
}
const album = albums.find(a => a.id === currentAlbumId);
album.images[currentEditIndex].data = document.getElementById('editTarget').src;
saveData();
renderGallery();
closeModal();
}
function closeModal() {
document.getElementById('editorModal').classList.remove('open');
if (cropper) {
cropper.destroy();
cropper = null;
}
}
// --- Download & Zip Logic ---
function downloadSingle(index) {
const album = albums.find(a => a.id === currentAlbumId);
const link = document.createElement('a');
link.href = album.images[index].data;
link.download = `image_${Date.now()}.jpg`;
link.click();
}
function deleteImage(index) {
if(confirm("Delete this image?")) {
const album = albums.find(a => a.id === currentAlbumId);
album.images.splice(index, 1);
saveData();
renderGallery();
renderAlbums();
}
}
function downloadAlbumZip() {
const album = albums.find(a => a.id === currentAlbumId);
if (!album || album.images.length === 0) return alert("Album is empty!");
const zip = new JSZip();
const folder = zip.folder(album.name);
// Add notes
if (album.notes) folder.file("notes.txt", album.notes);
// Add images
album.images.forEach((img, i) => {
// Remove 'data:image/jpeg;base64,' prefix for JSZip
const base64Data = img.data.split(',')[1];
if (base64Data) {
folder.file(`image_${i + 1}.jpg`, base64Data, {base64: true});
}
});
zip.generateAsync({type:"blob"}).then(function(content) {
saveAs(content, `${album.name}.zip`);
});
}
function saveData() {
try {
localStorage.setItem('galleryPro_albums', JSON.stringify(albums));
} catch (e) {
alert("Storage full! Delete some old images.");
}
}
// Init
renderAlbums();
</script>
</body>
</html>