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 +1 -1
- package/src/uiServer.js +15 -9
- package/ui/app.js +148 -9
- package/ui/index.html +551 -158
- package/ui/styles.css +289 -6
package/package.json
CHANGED
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
|
-
|
|
247
|
-
if (!
|
|
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 "${
|
|
265
|
+
command = `open "${folderPath}"`;
|
|
259
266
|
} else if (platform === 'win32') {
|
|
260
267
|
// Windows - use explorer to open in foreground
|
|
261
|
-
command = `explorer "${
|
|
268
|
+
command = `explorer "${folderPath}"`;
|
|
262
269
|
} else {
|
|
263
270
|
// Linux and others - suppress stderr to avoid Wayland/Gnome noise
|
|
264
|
-
command = `xdg-open "${
|
|
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', [
|
|
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:
|
|
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 = '
|
|
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
|
-
? '
|
|
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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
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
|
+
});
|