pulp-image 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulp-image",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A CLI tool for processing images with resize, format conversion, and optimization",
5
5
  "type": "module",
6
6
  "main": "bin/pulp.js",
package/src/uiServer.js CHANGED
@@ -243,11 +243,18 @@ export async function startUIServer(port = 3000) {
243
243
  // API endpoint to open folder in file manager
244
244
  app.post('/api/open-folder', async (req, res) => {
245
245
  try {
246
- const { path } = req.body;
247
- if (!path) {
246
+ let { path: folderPath } = req.body;
247
+ if (!folderPath) {
248
248
  return res.status(400).json({ error: 'Path required' });
249
249
  }
250
250
 
251
+ // Expand ~ to home directory
252
+ if (folderPath.startsWith('~')) {
253
+ folderPath = join(homedir(), folderPath.slice(1));
254
+ }
255
+ // Resolve to absolute path
256
+ folderPath = resolve(folderPath);
257
+
251
258
  // Determine OS and use appropriate command
252
259
  const platform = process.platform;
253
260
  let command;
@@ -255,13 +262,13 @@ export async function startUIServer(port = 3000) {
255
262
 
256
263
  if (platform === 'darwin') {
257
264
  // macOS
258
- command = `open "${path}"`;
265
+ command = `open "${folderPath}"`;
259
266
  } else if (platform === 'win32') {
260
267
  // Windows - use explorer to open in foreground
261
- command = `explorer "${path}"`;
268
+ command = `explorer "${folderPath}"`;
262
269
  } else {
263
270
  // Linux and others - suppress stderr to avoid Wayland/Gnome noise
264
- command = `xdg-open "${path}" 2>/dev/null || true`;
271
+ command = `xdg-open "${folderPath}" 2>/dev/null || true`;
265
272
  // Use shell to properly handle stderr redirection
266
273
  options = { shell: '/bin/bash' };
267
274
  }
@@ -271,7 +278,7 @@ export async function startUIServer(port = 3000) {
271
278
  if (platform === 'linux' || (platform !== 'darwin' && platform !== 'win32')) {
272
279
  // Use spawn with stderr redirected instead of execAsync for better control
273
280
  await new Promise((resolve, reject) => {
274
- const child = spawn('xdg-open', [path], {
281
+ const child = spawn('xdg-open', [folderPath], {
275
282
  stdio: ['ignore', 'ignore', 'ignore'], // Suppress all output
276
283
  detached: true
277
284
  });
@@ -288,17 +295,16 @@ export async function startUIServer(port = 3000) {
288
295
  console.error('Error opening folder:', execError);
289
296
  res.status(500).json({
290
297
  error: 'Could not open folder automatically.',
291
- path: path // Include path so UI can display it
298
+ path: folderPath // Include path so UI can display it
292
299
  });
293
300
  }
294
301
 
295
302
  } catch (error) {
296
303
  // Don't expose raw errors to user
297
304
  console.error('Error opening folder:', error);
298
- const { path } = req.body;
299
305
  res.status(500).json({
300
306
  error: 'Could not open folder automatically.',
301
- path: path || null // Include path if available
307
+ path: req.body?.path || null // Include path if available
302
308
  });
303
309
  }
304
310
  });
package/ui/app.js CHANGED
@@ -185,7 +185,7 @@ function setupForm() {
185
185
  if (useCustomOutput.checked) {
186
186
  outputDir.readOnly = false;
187
187
  outputDir.style.background = 'white';
188
- outputDirHelper.textContent = 'Specify a custom output directory. Use ~ for home directory.';
188
+ outputDirHelper.textContent = 'Use ~ as a shortcut for your home folder (e.g., ~/my-images), or enter a full path.';
189
189
  await validateCustomOutputPath();
190
190
  } else {
191
191
  outputDir.readOnly = true;
@@ -195,10 +195,12 @@ function setupForm() {
195
195
  }
196
196
  });
197
197
 
198
- // Validate custom output path on input
198
+ // Validate custom output path on input and update resolvedOutputPath
199
199
  outputDir.addEventListener('blur', async () => {
200
200
  if (useCustomOutput.checked) {
201
201
  await validateCustomOutputPath();
202
+ // Update resolvedOutputPath with the custom value
203
+ resolvedOutputPath = outputDir.value;
202
204
  }
203
205
  });
204
206
 
@@ -581,7 +583,7 @@ async function validateCustomOutputPath() {
581
583
  const helper = document.getElementById('output-dir-helper');
582
584
  if (helper) {
583
585
  helper.textContent = useCustomOutput.checked
584
- ? 'Specify a custom output directory. Use ~ for home directory.'
586
+ ? 'Use ~ as a shortcut for your home folder (e.g., ~/my-images), or enter a full path.'
585
587
  : 'Files will be saved in a new folder inside your home directory.';
586
588
  }
587
589
  }
@@ -734,7 +736,13 @@ async function handleSubmit(e) {
734
736
  }
735
737
 
736
738
  // Validate output directory before processing
737
- let outputPath = resolvedOutputPath || (outputDir ? outputDir.value : null);
739
+ // When custom output is enabled, always use the current input value
740
+ let outputPath;
741
+ if (useCustomOutput && useCustomOutput.checked) {
742
+ outputPath = outputDir ? outputDir.value : null;
743
+ } else {
744
+ outputPath = resolvedOutputPath || (outputDir ? outputDir.value : null);
745
+ }
738
746
  if (!outputPath || outputPath.trim() === '') {
739
747
  alert('Error: Output directory is not set. Please wait for the directory path to be resolved, or specify a custom output directory.');
740
748
  return;
@@ -757,10 +765,7 @@ async function handleSubmit(e) {
757
765
 
758
766
  try {
759
767
  // Use stored resolved path or current value
760
- // Expand ~ to home directory (will be done on server, but prepare it)
761
- if (outputPath.startsWith('~')) {
762
- outputPath = outputPath.replace('~', '');
763
- }
768
+ // Note: ~ expansion is handled by the server
764
769
 
765
770
  const config = {
766
771
  width: widthInput.value ? parseInt(widthInput.value, 10) : null,
@@ -809,7 +814,8 @@ async function handleSubmit(e) {
809
814
  const results = await response.json();
810
815
 
811
816
  // Update output directory with resolved path
812
- if (results.outputPath) {
817
+ // Only update if NOT using custom output (to preserve user's custom path)
818
+ if (results.outputPath && !useCustomOutput.checked) {
813
819
  if (outputDir) {
814
820
  outputDir.value = results.outputPath;
815
821
  }
@@ -1151,3 +1157,136 @@ function resetForm() {
1151
1157
  resultsSection.style.display = 'none';
1152
1158
  }
1153
1159
 
1160
+
1161
+ // Terminal Example Copy Functionality
1162
+ function setupTerminalCopyButtons() {
1163
+ document.querySelectorAll('.terminal-example-copy').forEach(btn => {
1164
+ btn.addEventListener('click', async (e) => {
1165
+ e.preventDefault();
1166
+ const body = btn.closest('.terminal-example-body');
1167
+ const textToCopy = body?.dataset.copy;
1168
+
1169
+ if (!textToCopy) return;
1170
+
1171
+ try {
1172
+ await navigator.clipboard.writeText(textToCopy);
1173
+
1174
+ // Show success state
1175
+ const copyIcon = btn.querySelector('.copy-icon');
1176
+ const checkIcon = btn.querySelector('.check-icon');
1177
+
1178
+ if (copyIcon && checkIcon) {
1179
+ copyIcon.style.display = 'none';
1180
+ checkIcon.style.display = 'block';
1181
+
1182
+ setTimeout(() => {
1183
+ copyIcon.style.display = 'block';
1184
+ checkIcon.style.display = 'none';
1185
+ }, 2000);
1186
+ }
1187
+ } catch (err) {
1188
+ console.error('Failed to copy:', err);
1189
+ }
1190
+ });
1191
+ });
1192
+ }
1193
+
1194
+ // Back to Top Button (works for all tabs)
1195
+ function setupBackToTop() {
1196
+ const backToTop = document.getElementById('back-to-top');
1197
+ if (!backToTop) return;
1198
+
1199
+ // Show/hide based on scroll position
1200
+ window.addEventListener('scroll', () => {
1201
+ backToTop.style.display = window.scrollY > 300 ? 'flex' : 'none';
1202
+ });
1203
+
1204
+ // Click handler
1205
+ backToTop.addEventListener('click', () => {
1206
+ window.scrollTo({ top: 0, behavior: 'smooth' });
1207
+ });
1208
+ }
1209
+
1210
+ // Help Sub-Navigation
1211
+ function setupHelpSubnav() {
1212
+ const subnav = document.getElementById('help-subnav');
1213
+ if (!subnav) return;
1214
+
1215
+ const links = subnav.querySelectorAll('.help-subnav-link');
1216
+ const helpTab = document.getElementById('help-tab');
1217
+
1218
+ // Set first link as active initially
1219
+ if (links.length > 0) {
1220
+ links[0].classList.add('active');
1221
+ }
1222
+
1223
+ // Flag to temporarily disable scroll-based updates after click
1224
+ let isClickScrolling = false;
1225
+
1226
+ // Navigation links - smooth scroll
1227
+ links.forEach(link => {
1228
+ link.addEventListener('click', (e) => {
1229
+ e.preventDefault();
1230
+ const targetId = link.getAttribute('href').slice(1);
1231
+ const target = document.getElementById(targetId);
1232
+
1233
+ if (target) {
1234
+ // Set active immediately on click
1235
+ links.forEach(l => l.classList.remove('active'));
1236
+ link.classList.add('active');
1237
+
1238
+ // Disable scroll-based updates during animation
1239
+ isClickScrolling = true;
1240
+
1241
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1242
+
1243
+ // Re-enable scroll updates after animation completes
1244
+ setTimeout(() => {
1245
+ isClickScrolling = false;
1246
+ }, 800);
1247
+ }
1248
+ });
1249
+ });
1250
+
1251
+ // Track scroll position to update active nav link
1252
+ function updateActiveOnScroll() {
1253
+ // Skip if we're in the middle of a click-triggered scroll
1254
+ if (isClickScrolling) return;
1255
+
1256
+ // Only track when help tab is visible
1257
+ if (!helpTab || !helpTab.classList.contains('active')) return;
1258
+
1259
+ // Update active nav link based on scroll position
1260
+ let currentSection = '';
1261
+ const scrollOffset = 80; // Smaller offset for more accurate detection
1262
+
1263
+ links.forEach(link => {
1264
+ const targetId = link.getAttribute('href').slice(1);
1265
+ const section = document.getElementById(targetId);
1266
+ if (section) {
1267
+ const rect = section.getBoundingClientRect();
1268
+ if (rect.top <= scrollOffset) {
1269
+ currentSection = targetId;
1270
+ }
1271
+ }
1272
+ });
1273
+
1274
+ // Update active class
1275
+ if (currentSection) {
1276
+ links.forEach(link => {
1277
+ const targetId = link.getAttribute('href').slice(1);
1278
+ link.classList.toggle('active', targetId === currentSection);
1279
+ });
1280
+ }
1281
+ }
1282
+
1283
+ // Listen to window scroll
1284
+ window.addEventListener('scroll', updateActiveOnScroll);
1285
+ }
1286
+
1287
+ // Initialize when DOM is ready
1288
+ document.addEventListener('DOMContentLoaded', () => {
1289
+ setupTerminalCopyButtons();
1290
+ setupBackToTop();
1291
+ setupHelpSubnav();
1292
+ });