sugakrit6 commited on
Commit
7ffc020
·
verified ·
1 Parent(s): 94cab96

Update gallery.html

Browse files
Files changed (1) hide show
  1. gallery.html +359 -174
gallery.html CHANGED
@@ -3,14 +3,21 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Grain & Gallery Studio</title>
 
 
 
 
 
 
7
  <style>
8
  :root {
9
  --bg: #121212;
10
  --surface: #1e1e1e;
11
  --primary: #bb86fc;
 
12
  --text: #e0e0e0;
13
- --danger: #cf6679;
14
  }
15
 
16
  body {
@@ -25,15 +32,16 @@
25
 
26
  /* --- Sidebar --- */
27
  .sidebar {
28
- width: 250px;
29
  background-color: var(--surface);
30
  padding: 20px;
31
  display: flex;
32
  flex-direction: column;
33
- border-right: 1px solid #333;
 
34
  }
35
 
36
- .sidebar h2 { margin-top: 0; font-size: 1.2rem; color: var(--primary); }
37
 
38
  .album-list {
39
  list-style: none;
@@ -44,48 +52,75 @@
44
  }
45
 
46
  .album-item {
47
- padding: 10px;
48
  cursor: pointer;
49
- border-radius: 4px;
50
- margin-bottom: 5px;
51
  display: flex;
52
  justify-content: space-between;
53
  align-items: center;
 
 
54
  }
55
 
56
  .album-item:hover { background-color: #333; }
57
  .album-item.active { background-color: var(--primary); color: #000; }
58
 
59
- .lock-icon { font-size: 0.8rem; opacity: 0.7; }
 
 
 
 
 
 
 
 
 
60
 
61
  button {
62
  background-color: var(--primary);
63
  border: none;
64
  color: #000;
65
- padding: 10px;
66
  border-radius: 4px;
67
  cursor: pointer;
68
  font-weight: bold;
69
- margin-top: 10px;
70
  }
71
-
72
- button.secondary { background-color: #333; color: white; border: 1px solid #555; }
 
73
 
74
  /* --- Main Content --- */
75
  .main {
76
  flex-grow: 1;
77
- padding: 20px;
78
- overflow-y: auto;
79
- position: relative;
80
  }
81
 
82
  .header {
83
- display: flex;
84
- justify-content: space-between;
85
- align-items: center;
86
- margin-bottom: 20px;
87
- border-bottom: 1px solid #333;
88
- padding-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
 
91
  .gallery-grid {
@@ -98,11 +133,11 @@
98
  background-color: var(--surface);
99
  border-radius: 8px;
100
  overflow: hidden;
101
- transition: transform 0.2s;
 
 
102
  }
103
 
104
- .img-card:hover { transform: translateY(-5px); }
105
-
106
  .img-card img {
107
  width: 100%;
108
  height: 200px;
@@ -110,130 +145,158 @@
110
  display: block;
111
  }
112
 
113
- .empty-state {
114
- text-align: center;
115
- opacity: 0.5;
116
- margin-top: 50px;
 
 
 
 
 
117
  }
118
 
 
 
 
 
 
 
 
119
  /* --- Editor Modal --- */
120
  .modal {
121
  position: fixed;
122
  top: 0; left: 0; right: 0; bottom: 0;
123
- background: rgba(0,0,0,0.9);
124
  display: none;
125
  flex-direction: column;
126
- align-items: center;
127
- justify-content: center;
128
- z-index: 1000;
129
  }
130
-
131
  .modal.open { display: flex; }
132
 
133
- .canvas-container {
134
- max-width: 80%;
135
- max-height: 70%;
136
- border: 1px solid #555;
137
- margin-bottom: 20px;
138
  overflow: hidden;
 
 
 
139
  }
140
 
141
- canvas { max-width: 100%; max-height: 100%; display: block; }
142
-
143
- .controls {
144
  background: var(--surface);
145
- padding: 20px;
146
  border-radius: 8px;
147
  display: flex;
148
  gap: 20px;
149
  align-items: center;
 
 
150
  }
151
 
152
- input[type="range"] { accent-color: var(--primary); }
153
-
154
- /* Access Badge */
155
- .badge {
156
- padding: 4px 8px;
157
- border-radius: 4px;
158
- font-size: 0.8rem;
159
- font-weight: bold;
160
  }
161
- .badge.public { background: #2e7d32; color: white; }
162
- .badge.private { background: #c62828; color: white; }
 
163
 
164
  </style>
165
  </head>
166
  <body>
167
 
168
  <div class="sidebar">
169
- <h2>My Studio</h2>
 
 
 
 
 
 
 
 
 
 
170
  <div class="album-list" id="albumList">
171
  </div>
172
-
173
- <hr style="width:100%; border-color:#333;">
174
-
175
- <h3>New Album</h3>
176
- <input type="text" id="newAlbumName" placeholder="Album Name" style="width: 90%; padding: 8px; margin-bottom: 10px; background: #333; border: none; color: white;">
177
- <select id="newAlbumAccess" style="width: 96%; padding: 8px; margin-bottom: 10px; background: #333; border: none; color: white;">
178
- <option value="public">Allow All (Public)</option>
179
- <option value="private">Request Access (Private)</option>
180
- </select>
181
- <button onclick="createAlbum()">Create Album</button>
182
  </div>
183
 
184
  <div class="main">
185
  <div class="header">
186
- <div>
187
- <h1 id="currentAlbumTitle">Select an Album</h1>
188
- <span id="accessBadge" class="badge public" style="display:none;">Public</span>
189
  </div>
190
- <div id="uploadSection" style="display:none;">
191
- <input type="file" id="fileInput" accept="image/*" style="display:none" onchange="loadImageForEditing(this)">
192
- <button onclick="document.getElementById('fileInput').click()">+ Upload & Edit</button>
 
 
 
 
 
 
 
193
  </div>
194
  </div>
195
 
196
- <div class="gallery-grid" id="galleryGrid">
197
- <div class="empty-state">Select or create an album to begin.</div>
 
 
198
  </div>
199
  </div>
200
 
201
  <div class="modal" id="editorModal">
202
- <div class="canvas-container">
203
- <canvas id="editorCanvas"></canvas>
204
  </div>
205
 
206
- <div class="controls">
207
- <div>
208
- <label>Grain Amount: <span id="grainValue">0</span></label><br>
209
- <input type="range" id="grainRange" min="0" max="100" value="0" oninput="applyGrainEffect()">
 
210
  </div>
211
- <div>
212
- <button onclick="saveImage()">Save to Album</button>
 
 
 
 
 
 
 
213
  <button class="secondary" onclick="closeModal()">Cancel</button>
214
  </div>
215
  </div>
216
  </div>
217
 
218
  <script>
219
- // --- State Management ---
220
- // Using localStorage to persist data in browser
221
- let albums = JSON.parse(localStorage.getItem('galleryAlbums')) || [];
222
  let currentAlbumId = null;
223
- let originalImage = null; // Stores the uploaded image for resetting filters
 
 
224
 
225
- // --- Album Logic ---
226
  function renderAlbums() {
227
  const list = document.getElementById('albumList');
228
  list.innerHTML = '';
229
-
230
  albums.forEach(album => {
231
  const li = document.createElement('li');
232
  li.className = `album-item ${currentAlbumId === album.id ? 'active' : ''}`;
233
- li.innerHTML = `
234
- <span>${album.name}</span>
235
- ${album.access === 'private' ? '<span class="lock-icon">🔒</span>' : ''}
236
- `;
237
  li.onclick = () => selectAlbum(album.id);
238
  list.appendChild(li);
239
  });
@@ -242,16 +305,9 @@
242
  function createAlbum() {
243
  const name = document.getElementById('newAlbumName').value;
244
  const access = document.getElementById('newAlbumAccess').value;
245
-
246
- if (!name) return alert("Please enter a name");
247
-
248
- const newAlbum = {
249
- id: Date.now(),
250
- name: name,
251
- access: access,
252
- images: []
253
- };
254
 
 
255
  albums.push(newAlbum);
256
  saveData();
257
  document.getElementById('newAlbumName').value = '';
@@ -261,124 +317,253 @@
261
 
262
  function selectAlbum(id) {
263
  currentAlbumId = id;
264
- renderAlbums();
 
 
 
 
 
 
 
265
  renderGallery();
 
266
  }
267
 
 
 
 
 
 
 
 
 
268
  function renderGallery() {
269
  const album = albums.find(a => a.id === currentAlbumId);
270
  const grid = document.getElementById('galleryGrid');
271
- const title = document.getElementById('currentAlbumTitle');
272
- const uploadSection = document.getElementById('uploadSection');
273
- const badge = document.getElementById('accessBadge');
274
-
275
- if (!album) return;
276
-
277
- // Update Header
278
- title.innerText = album.name;
279
- uploadSection.style.display = 'block';
280
-
281
- // Update Access Badge
282
- badge.style.display = 'inline-block';
283
- badge.className = `badge ${album.access}`;
284
- badge.innerText = album.access === 'public' ? 'Public (Allow All)' : 'Private (Request Only)';
285
-
286
- // Render Images
287
  grid.innerHTML = '';
 
288
  if (album.images.length === 0) {
289
- grid.innerHTML = '<div class="empty-state">No images yet. Upload one!</div>';
290
- } else {
291
- album.images.forEach(imgData => {
292
- const card = document.createElement('div');
293
- card.className = 'img-card';
294
- card.innerHTML = `<img src="${imgData}" />`;
295
- grid.appendChild(card);
296
- });
297
  }
298
- }
299
 
300
- // --- Editor & Grain Logic ---
301
- const modal = document.getElementById('editorModal');
302
- const canvas = document.getElementById('editorCanvas');
303
- const ctx = canvas.getContext('2d');
 
 
 
 
 
 
 
 
 
 
304
 
305
- function loadImageForEditing(input) {
 
306
  if (input.files && input.files[0]) {
307
  const reader = new FileReader();
308
- reader.onload = function(e) {
309
- const img = new Image();
310
- img.onload = function() {
311
- // Set Canvas size to match image (max 800px width for performance)
312
- const scale = Math.min(1, 800 / img.width);
313
- canvas.width = img.width * scale;
314
- canvas.height = img.height * scale;
315
-
316
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
317
- originalImage = ctx.getImageData(0, 0, canvas.width, canvas.height);
318
-
319
- modal.classList.add('open');
320
- document.getElementById('grainRange').value = 0;
321
- }
322
- img.src = e.target.result;
323
- }
324
  reader.readAsDataURL(input.files[0]);
325
  }
326
  }
327
 
328
- function applyGrainEffect() {
329
- if (!originalImage) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- const amount = parseInt(document.getElementById('grainRange').value);
332
- document.getElementById('grainValue').innerText = amount;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
- // Restore original before applying new noise
335
- ctx.putImageData(originalImage, 0, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- if (amount === 0) return;
 
 
338
 
 
 
 
 
 
 
 
 
339
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
340
  const data = imageData.data;
341
-
342
- // Loop through pixels and add noise
343
  for (let i = 0; i < data.length; i += 4) {
344
- // Generate random noise value (-amount to +amount)
345
  const noise = (Math.random() - 0.5) * amount * 2;
346
-
347
- // Apply to RGB
348
- data[i] = Math.min(255, Math.max(0, data[i] + noise)); // Red
349
- data[i+1] = Math.min(255, Math.max(0, data[i+1] + noise)); // Green
350
- data[i+2] = Math.min(255, Math.max(0, data[i+2] + noise)); // Blue
351
  }
352
-
353
  ctx.putImageData(imageData, 0, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  }
355
 
356
- function saveImage() {
 
 
 
 
 
 
 
 
 
357
  const album = albums.find(a => a.id === currentAlbumId);
358
- if (album) {
359
- // Convert canvas back to image URL
360
- const newData = canvas.toDataURL('image/jpeg', 0.8);
361
- album.images.push(newData);
 
 
 
 
 
 
362
  saveData();
363
  renderGallery();
364
- closeModal();
365
  }
366
  }
367
 
368
- function closeModal() {
369
- modal.classList.remove('open');
370
- document.getElementById('fileInput').value = ''; // Reset input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
 
373
  function saveData() {
374
  try {
375
- localStorage.setItem('galleryAlbums', JSON.stringify(albums));
376
  } catch (e) {
377
- alert("Storage full! Cannot save more images in this demo.");
378
  }
379
  }
380
 
381
- // Initialize
382
  renderAlbums();
383
  </script>
384
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gallery Pro: Grain & Notes</title>
7
+
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
12
+
13
  <style>
14
  :root {
15
  --bg: #121212;
16
  --surface: #1e1e1e;
17
  --primary: #bb86fc;
18
+ --secondary: #03dac6;
19
  --text: #e0e0e0;
20
+ --border: #333;
21
  }
22
 
23
  body {
 
32
 
33
  /* --- Sidebar --- */
34
  .sidebar {
35
+ width: 260px;
36
  background-color: var(--surface);
37
  padding: 20px;
38
  display: flex;
39
  flex-direction: column;
40
+ border-right: 1px solid var(--border);
41
+ flex-shrink: 0;
42
  }
43
 
44
+ .sidebar h2 { color: var(--primary); margin-top: 0; }
45
 
46
  .album-list {
47
  list-style: none;
 
52
  }
53
 
54
  .album-item {
55
+ padding: 12px;
56
  cursor: pointer;
57
+ border-radius: 6px;
58
+ margin-bottom: 8px;
59
  display: flex;
60
  justify-content: space-between;
61
  align-items: center;
62
+ background: #252525;
63
+ transition: 0.2s;
64
  }
65
 
66
  .album-item:hover { background-color: #333; }
67
  .album-item.active { background-color: var(--primary); color: #000; }
68
 
69
+ input, select, textarea {
70
+ background: #2b2b2b;
71
+ border: 1px solid #444;
72
+ color: white;
73
+ padding: 8px;
74
+ border-radius: 4px;
75
+ width: 100%;
76
+ box-sizing: border-box;
77
+ margin-bottom: 10px;
78
+ }
79
 
80
  button {
81
  background-color: var(--primary);
82
  border: none;
83
  color: #000;
84
+ padding: 10px 15px;
85
  border-radius: 4px;
86
  cursor: pointer;
87
  font-weight: bold;
88
+ transition: 0.2s;
89
  }
90
+ button:hover { opacity: 0.9; }
91
+ button.secondary { background-color: #444; color: white; }
92
+ button.action-btn { background-color: var(--secondary); color: #000; margin-right: 5px; }
93
 
94
  /* --- Main Content --- */
95
  .main {
96
  flex-grow: 1;
97
+ display: flex;
98
+ flex-direction: column;
99
+ overflow: hidden;
100
  }
101
 
102
  .header {
103
+ padding: 20px;
104
+ border-bottom: 1px solid var(--border);
105
+ background: var(--bg);
106
+ }
107
+
108
+ .top-bar { display: flex; justify-content: space-between; align-items: center; }
109
+ .action-bar { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
110
+
111
+ .notes-area {
112
+ width: 100%;
113
+ height: 60px;
114
+ margin-top: 10px;
115
+ resize: none;
116
+ font-family: monospace;
117
+ font-size: 0.9rem;
118
+ }
119
+
120
+ .gallery-container {
121
+ flex-grow: 1;
122
+ padding: 20px;
123
+ overflow-y: auto;
124
  }
125
 
126
  .gallery-grid {
 
133
  background-color: var(--surface);
134
  border-radius: 8px;
135
  overflow: hidden;
136
+ position: relative;
137
+ group: hover;
138
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
139
  }
140
 
 
 
141
  .img-card img {
142
  width: 100%;
143
  height: 200px;
 
145
  display: block;
146
  }
147
 
148
+ .img-actions {
149
+ position: absolute;
150
+ bottom: 0; left: 0; right: 0;
151
+ background: rgba(0,0,0,0.8);
152
+ padding: 5px;
153
+ display: flex;
154
+ justify-content: space-around;
155
+ transform: translateY(100%);
156
+ transition: transform 0.2s;
157
  }
158
 
159
+ .img-card:hover .img-actions { transform: translateY(0); }
160
+
161
+ .img-btn {
162
+ background: none; border: none; color: white; font-size: 1.2rem; cursor: pointer; padding: 5px;
163
+ }
164
+ .img-btn:hover { color: var(--primary); }
165
+
166
  /* --- Editor Modal --- */
167
  .modal {
168
  position: fixed;
169
  top: 0; left: 0; right: 0; bottom: 0;
170
+ background: rgba(0,0,0,0.95);
171
  display: none;
172
  flex-direction: column;
173
+ z-index: 2000;
174
+ padding: 20px;
 
175
  }
 
176
  .modal.open { display: flex; }
177
 
178
+ .editor-workspace {
179
+ flex-grow: 1;
180
+ display: flex;
181
+ justify-content: center;
182
+ align-items: center;
183
  overflow: hidden;
184
+ background: #000;
185
+ border: 1px solid #333;
186
+ margin-bottom: 10px;
187
  }
188
 
189
+ .editor-workspace img { max-width: 100%; max-height: 80vh; display: block; }
190
+
191
+ .editor-toolbar {
192
  background: var(--surface);
193
+ padding: 15px;
194
  border-radius: 8px;
195
  display: flex;
196
  gap: 20px;
197
  align-items: center;
198
+ justify-content: center;
199
+ flex-wrap: wrap;
200
  }
201
 
202
+ .tool-group {
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 10px;
206
+ border-right: 1px solid #444;
207
+ padding-right: 20px;
 
 
208
  }
209
+ .tool-group:last-child { border: none; }
210
+
211
+ label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; }
212
 
213
  </style>
214
  </head>
215
  <body>
216
 
217
  <div class="sidebar">
218
+ <h2>Gallery Pro</h2>
219
+
220
+ <div style="background: #252525; padding: 10px; border-radius: 6px;">
221
+ <input type="text" id="newAlbumName" placeholder="New Album Name">
222
+ <select id="newAlbumAccess">
223
+ <option value="public">🔓 Allow All</option>
224
+ <option value="private">🔒 Limited Access</option>
225
+ </select>
226
+ <button onclick="createAlbum()" style="width:100%">+ Create</button>
227
+ </div>
228
+
229
  <div class="album-list" id="albumList">
230
  </div>
 
 
 
 
 
 
 
 
 
 
231
  </div>
232
 
233
  <div class="main">
234
  <div class="header">
235
+ <div class="top-bar">
236
+ <h1 id="currentAlbumTitle" style="margin:0">Select Album</h1>
237
+ <span id="accessBadge" style="padding: 5px 10px; border-radius: 4px; background:#333; font-size: 0.8rem;"></span>
238
  </div>
239
+
240
+ <textarea id="albumNotes" class="notes-area" placeholder="Write notes about this album..." oninput="saveNotes()"></textarea>
241
+
242
+ <div class="action-bar" id="actionBar" style="display:none;">
243
+ <input type="file" id="fileInput" accept="image/*" style="display:none" onchange="handleFileUpload(this)">
244
+ <button onclick="document.getElementById('fileInput').click()">📁 Upload File</button>
245
+
246
+ <button class="secondary" onclick="addFromUrl()">🌐 Add from URL</button>
247
+
248
+ <button class="action-btn" onclick="downloadAlbumZip()">💾 Download Album (.zip)</button>
249
  </div>
250
  </div>
251
 
252
+ <div class="gallery-container">
253
+ <div class="gallery-grid" id="galleryGrid">
254
+ <p style="opacity:0.5; margin: 20px;">Select an album to start.</p>
255
+ </div>
256
  </div>
257
  </div>
258
 
259
  <div class="modal" id="editorModal">
260
+ <div class="editor-workspace">
261
+ <img id="editTarget" src="" alt="Editing...">
262
  </div>
263
 
264
+ <div class="editor-toolbar">
265
+ <div class="tool-group">
266
+ <label>Grain</label>
267
+ <input type="range" id="grainRange" min="0" max="100" value="0">
268
+ <button class="secondary" onclick="applyGrain()">Apply</button>
269
  </div>
270
+
271
+ <div class="tool-group">
272
+ <button class="secondary" onclick="rotateImage()">↻ Rotate 90°</button>
273
+ <button class="secondary" id="btnCrop" onclick="toggleCrop()">✂ Crop</button>
274
+ </div>
275
+
276
+ <div class="tool-group" style="border:none">
277
+ <button class="secondary" onclick="restoreOriginal()" style="background:#cf6679; color:white">Restore</button>
278
+ <button onclick="saveEdits()">✅ Save Changes</button>
279
  <button class="secondary" onclick="closeModal()">Cancel</button>
280
  </div>
281
  </div>
282
  </div>
283
 
284
  <script>
285
+ // --- State ---
286
+ let albums = JSON.parse(localStorage.getItem('galleryPro_albums')) || [];
 
287
  let currentAlbumId = null;
288
+ let cropper = null;
289
+ let currentEditIndex = -1; // Which image in the array are we editing?
290
+ let originalImgData = null; // Backup for restore
291
 
292
+ // --- Album Management ---
293
  function renderAlbums() {
294
  const list = document.getElementById('albumList');
295
  list.innerHTML = '';
 
296
  albums.forEach(album => {
297
  const li = document.createElement('li');
298
  li.className = `album-item ${currentAlbumId === album.id ? 'active' : ''}`;
299
+ li.innerHTML = `<span>${album.name} <small>(${album.images.length})</small></span> ${album.access === 'private' ? '🔒' : '🔓'}`;
 
 
 
300
  li.onclick = () => selectAlbum(album.id);
301
  list.appendChild(li);
302
  });
 
305
  function createAlbum() {
306
  const name = document.getElementById('newAlbumName').value;
307
  const access = document.getElementById('newAlbumAccess').value;
308
+ if (!name) return alert("Enter a name");
 
 
 
 
 
 
 
 
309
 
310
+ const newAlbum = { id: Date.now(), name, access, notes: "", images: [] };
311
  albums.push(newAlbum);
312
  saveData();
313
  document.getElementById('newAlbumName').value = '';
 
317
 
318
  function selectAlbum(id) {
319
  currentAlbumId = id;
320
+ const album = albums.find(a => a.id === id);
321
+
322
+ document.getElementById('currentAlbumTitle').innerText = album.name;
323
+ document.getElementById('albumNotes').value = album.notes || "";
324
+ document.getElementById('accessBadge').innerText = album.access === 'public' ? "Public Access" : "Restricted Access";
325
+ document.getElementById('accessBadge').style.background = album.access === 'public' ? "#2e7d32" : "#c62828";
326
+ document.getElementById('actionBar').style.display = 'flex';
327
+
328
  renderGallery();
329
+ renderAlbums(); // Update active state
330
  }
331
 
332
+ function saveNotes() {
333
+ if (!currentAlbumId) return;
334
+ const album = albums.find(a => a.id === currentAlbumId);
335
+ album.notes = document.getElementById('albumNotes').value;
336
+ saveData();
337
+ }
338
+
339
+ // --- Gallery Rendering ---
340
  function renderGallery() {
341
  const album = albums.find(a => a.id === currentAlbumId);
342
  const grid = document.getElementById('galleryGrid');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  grid.innerHTML = '';
344
+
345
  if (album.images.length === 0) {
346
+ grid.innerHTML = '<p style="opacity:0.5; width:100%">No images. Upload or paste a URL.</p>';
347
+ return;
 
 
 
 
 
 
348
  }
 
349
 
350
+ album.images.forEach((img, index) => {
351
+ const card = document.createElement('div');
352
+ card.className = 'img-card';
353
+ card.innerHTML = `
354
+ <img src="${img.data}" alt="Gallery Image">
355
+ <div class="img-actions">
356
+ <button class="img-btn" onclick="openEditor(${index})" title="Edit">✏️</button>
357
+ <button class="img-btn" onclick="downloadSingle(${index})" title="Download">⬇️</button>
358
+ <button class="img-btn" onclick="deleteImage(${index})" title="Delete" style="color:#cf6679">🗑️</button>
359
+ </div>
360
+ `;
361
+ grid.appendChild(card);
362
+ });
363
+ }
364
 
365
+ // --- Adding Images ---
366
+ function handleFileUpload(input) {
367
  if (input.files && input.files[0]) {
368
  const reader = new FileReader();
369
+ reader.onload = (e) => addToAlbum(e.target.result);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  reader.readAsDataURL(input.files[0]);
371
  }
372
  }
373
 
374
+ function addFromUrl() {
375
+ const url = prompt("Paste Image URL:");
376
+ if (url) {
377
+ // We attempt to draw it to canvas to bypass basic hotlinking limits,
378
+ // but CORS might block it. If it blocks, we save the URL string directly.
379
+ const img = new Image();
380
+ img.crossOrigin = "Anonymous";
381
+ img.onload = function() {
382
+ const canvas = document.createElement('canvas');
383
+ canvas.width = img.width;
384
+ canvas.height = img.height;
385
+ const ctx = canvas.getContext('2d');
386
+ ctx.drawImage(img, 0, 0);
387
+ try {
388
+ addToAlbum(canvas.toDataURL("image/jpeg"));
389
+ } catch (e) {
390
+ // Tainted canvas fallback
391
+ addToAlbum(url);
392
+ }
393
+ };
394
+ img.onerror = () => addToAlbum(url); // Fallback if CORS fails completely
395
+ img.src = url;
396
+ }
397
+ }
398
 
399
+ function addToAlbum(dataString) {
400
+ const album = albums.find(a => a.id === currentAlbumId);
401
+ album.images.push({ data: dataString, date: new Date().toISOString() });
402
+ saveData();
403
+ renderGallery();
404
+ renderAlbums();
405
+ }
406
+
407
+ // --- Editor Logic ---
408
+ function openEditor(index) {
409
+ const album = albums.find(a => a.id === currentAlbumId);
410
+ currentEditIndex = index;
411
+ const imgData = album.images[index].data;
412
+ originalImgData = imgData; // Save for restore
413
+
414
+ const imgEl = document.getElementById('editTarget');
415
+ imgEl.src = imgData;
416
+
417
+ document.getElementById('editorModal').classList.add('open');
418
+
419
+ // Wait for image to load before allowing crop
420
+ imgEl.onload = () => {
421
+ if (cropper) cropper.destroy();
422
+ };
423
+ }
424
+
425
+ function toggleCrop() {
426
+ const imgEl = document.getElementById('editTarget');
427
+ if (cropper) {
428
+ // Apply crop
429
+ const canvas = cropper.getCroppedCanvas();
430
+ imgEl.src = canvas.toDataURL();
431
+ cropper.destroy();
432
+ cropper = null;
433
+ document.getElementById('btnCrop').innerText = "✂ Crop";
434
+ document.getElementById('btnCrop').classList.add('secondary');
435
+ } else {
436
+ // Start crop
437
+ cropper = new Cropper(imgEl, { viewMode: 1 });
438
+ document.getElementById('btnCrop').innerText = "Apply Crop";
439
+ document.getElementById('btnCrop').classList.remove('secondary');
440
+ }
441
+ }
442
 
443
+ function rotateImage() {
444
+ if (cropper) cropper.rotate(90);
445
+ else {
446
+ // Manual canvas rotation if cropper isn't active
447
+ const img = document.getElementById('editTarget');
448
+ const canvas = document.createElement('canvas');
449
+ canvas.width = img.naturalHeight;
450
+ canvas.height = img.naturalWidth;
451
+ const ctx = canvas.getContext('2d');
452
+ ctx.translate(canvas.width/2, canvas.height/2);
453
+ ctx.rotate(90 * Math.PI / 180);
454
+ ctx.drawImage(img, -img.naturalWidth/2, -img.naturalHeight/2);
455
+ img.src = canvas.toDataURL();
456
+ }
457
+ }
458
 
459
+ function applyGrain() {
460
+ const amount = parseInt(document.getElementById('grainRange').value);
461
+ if(amount === 0) return;
462
 
463
+ const img = document.getElementById('editTarget');
464
+ const canvas = document.createElement('canvas');
465
+ canvas.width = img.naturalWidth || img.width;
466
+ canvas.height = img.naturalHeight || img.height;
467
+ const ctx = canvas.getContext('2d');
468
+
469
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
470
+
471
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
472
  const data = imageData.data;
473
+
 
474
  for (let i = 0; i < data.length; i += 4) {
 
475
  const noise = (Math.random() - 0.5) * amount * 2;
476
+ data[i] += noise;
477
+ data[i+1] += noise;
478
+ data[i+2] += noise;
 
 
479
  }
480
+
481
  ctx.putImageData(imageData, 0, 0);
482
+ img.src = canvas.toDataURL();
483
+ document.getElementById('grainRange').value = 0; // Reset slider
484
+ }
485
+
486
+ function restoreOriginal() {
487
+ if (cropper) cropper.destroy();
488
+ cropper = null;
489
+ document.getElementById('editTarget').src = originalImgData;
490
+ }
491
+
492
+ function saveEdits() {
493
+ if (cropper) {
494
+ const canvas = cropper.getCroppedCanvas();
495
+ document.getElementById('editTarget').src = canvas.toDataURL();
496
+ cropper.destroy();
497
+ cropper = null;
498
+ }
499
+
500
+ const album = albums.find(a => a.id === currentAlbumId);
501
+ album.images[currentEditIndex].data = document.getElementById('editTarget').src;
502
+ saveData();
503
+ renderGallery();
504
+ closeModal();
505
  }
506
 
507
+ function closeModal() {
508
+ document.getElementById('editorModal').classList.remove('open');
509
+ if (cropper) {
510
+ cropper.destroy();
511
+ cropper = null;
512
+ }
513
+ }
514
+
515
+ // --- Download & Zip Logic ---
516
+ function downloadSingle(index) {
517
  const album = albums.find(a => a.id === currentAlbumId);
518
+ const link = document.createElement('a');
519
+ link.href = album.images[index].data;
520
+ link.download = `image_${Date.now()}.jpg`;
521
+ link.click();
522
+ }
523
+
524
+ function deleteImage(index) {
525
+ if(confirm("Delete this image?")) {
526
+ const album = albums.find(a => a.id === currentAlbumId);
527
+ album.images.splice(index, 1);
528
  saveData();
529
  renderGallery();
530
+ renderAlbums();
531
  }
532
  }
533
 
534
+ function downloadAlbumZip() {
535
+ const album = albums.find(a => a.id === currentAlbumId);
536
+ if (!album || album.images.length === 0) return alert("Album is empty!");
537
+
538
+ const zip = new JSZip();
539
+ const folder = zip.folder(album.name);
540
+
541
+ // Add notes
542
+ if (album.notes) folder.file("notes.txt", album.notes);
543
+
544
+ // Add images
545
+ album.images.forEach((img, i) => {
546
+ // Remove 'data:image/jpeg;base64,' prefix for JSZip
547
+ const base64Data = img.data.split(',')[1];
548
+ if (base64Data) {
549
+ folder.file(`image_${i + 1}.jpg`, base64Data, {base64: true});
550
+ }
551
+ });
552
+
553
+ zip.generateAsync({type:"blob"}).then(function(content) {
554
+ saveAs(content, `${album.name}.zip`);
555
+ });
556
  }
557
 
558
  function saveData() {
559
  try {
560
+ localStorage.setItem('galleryPro_albums', JSON.stringify(albums));
561
  } catch (e) {
562
+ alert("Storage full! Delete some old images.");
563
  }
564
  }
565
 
566
+ // Init
567
  renderAlbums();
568
  </script>
569
  </body>