diff --git a/js/load_audio_ui.js b/js/load_audio_ui.js index 0406b5e..5964f3c 100644 --- a/js/load_audio_ui.js +++ b/js/load_audio_ui.js @@ -245,9 +245,6 @@ app.registerExtension({ const widget = this.addDOMWidget("audio_ui", "audio_ui", container); // --- DEFAULT SIZE FOR NEW NODES --- - // We set the width to 475 initially. - // If a workflow is loading, LiteGraph will overwrite this size - // automatically after this function finishes. this.size = [475, this.computeSize()[1]]; widget.computeSize = function(width) { @@ -259,6 +256,49 @@ app.registerExtension({ const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio"); const startWidget = node.widgets && node.widgets.find(w => w.name === "start_time"); const endWidget = node.widgets && node.widgets.find(w => w.name === "end_time"); + const durationWidget = node.widgets && node.widgets.find(w => w.name === "duration"); + + let duration = 0; + let dragging = null; + let dragOffset = 0; + let dragSelectionWidth = 0; + let isUpdatingDuration = false; // Flag to prevent infinite loops + + // Hook into the Python-generated native duration widget + if (durationWidget) { + const origCallback = durationWidget.callback; + durationWidget.callback = function(v) { + // If we're internally triggering it, ignore it + if (!duration || isUpdatingDuration) { + if (origCallback) origCallback.apply(this, arguments); + return; + } + + isUpdatingDuration = true; + let d = parseFloat(v) || 0; + if (d < 0) d = 0; + if (d > duration) d = duration; + + let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; + let newStart = s; + let newEnd = s + d; + + // If adding duration pushes past the end of the audio, shift the start time back instead + if (newEnd > duration) { + newEnd = duration; + newStart = duration - d; + } + + if (startWidget) startWidget.value = parseFloat(newStart.toFixed(2)); + if (endWidget) endWidget.value = parseFloat(newEnd.toFixed(2)); + + updateUI(true); + app.graph.setDirtyCanvas(true, false); + + if (origCallback) origCallback.apply(this, arguments); + isUpdatingDuration = false; + }; + } if (audioWidget) { const updateAudio = () => { @@ -306,9 +346,6 @@ app.registerExtension({ } }; - let duration = 0; - let dragging = null; - const formatTime = (secs) => { if (secs < 60) return secs.toFixed(1) + "s"; const m = Math.floor(secs / 60); @@ -360,7 +397,16 @@ app.registerExtension({ endHandle.style.left = `${ePct}%`; fill.style.left = `${sPct}%`; fill.style.width = `${ePct - sPct}%`; - trimLength.textContent = `Trimmed: ${formatTime(e - s)}`; + + // Sync native duration widget seamlessly to match UI handles + const currentDur = parseFloat((e - s).toFixed(2)); + trimLength.textContent = `Trimmed: ${currentDur}s`; + if (durationWidget && durationWidget.value !== currentDur) { + isUpdatingDuration = true; + durationWidget.value = currentDur; + isUpdatingDuration = false; + } + if (syncPlayer && audioEl.readyState >= 1) { audioEl.currentTime = s; } }; @@ -370,13 +416,13 @@ app.registerExtension({ // Handle trim reset for new audio selection if (node._should_reset_trim) { if (startWidget) startWidget.value = 0; - if (endWidget) endWidget.value = parseFloat(duration.toFixed(3)); + if (endWidget) endWidget.value = parseFloat(duration.toFixed(2)); node._should_reset_trim = false; } else { // Default clamping logic for initial load or out-of-bounds saved values let e = endWidget ? parseFloat(endWidget.value) || 0 : 0; if (endWidget && (e === 0 || e > duration)) { - endWidget.value = parseFloat(duration.toFixed(3)); + endWidget.value = parseFloat(duration.toFixed(2)); } } @@ -414,12 +460,20 @@ app.registerExtension({ const val = (x / rect.width) * duration; let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration; - if (Math.abs(val - s) < Math.abs(val - e_val)) { + + // Define a "handle tolerance" zone (approx 10px on each side) to prioritize resizing over dragging + const handleTolerance = (10 / rect.width) * duration; + + if (val > s + handleTolerance && val < e_val - handleTolerance) { + dragging = 'center'; + dragOffset = val - s; + dragSelectionWidth = e_val - s; + } else if (Math.abs(val - s) < Math.abs(val - e_val)) { dragging = 'start'; - if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3)); + if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(2)); } else { dragging = 'end'; - if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3)); + if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(2)); } updateUI(true); app.graph.setDirtyCanvas(true, false); sliderBox.setPointerCapture(e.pointerId); @@ -432,10 +486,25 @@ app.registerExtension({ const val = (x / rect.width) * duration; if (dragging === 'start') { let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration; - if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3)); + if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(2)); } else if (dragging === 'end') { const s = startWidget ? parseFloat(startWidget.value) || 0 : 0; - if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3)); + if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(2)); + } else if (dragging === 'center') { + let newStart = val - dragOffset; + let newEnd = newStart + dragSelectionWidth; + + // Clamp to bounds + if (newStart < 0) { + newStart = 0; + newEnd = dragSelectionWidth; + } else if (newEnd > duration) { + newEnd = duration; + newStart = duration - dragSelectionWidth; + } + + if(startWidget) startWidget.value = parseFloat(newStart.toFixed(2)); + if(endWidget) endWidget.value = parseFloat(newEnd.toFixed(2)); } updateUI(true); app.graph.setDirtyCanvas(true, false); }; diff --git a/load_audio_ui.py b/load_audio_ui.py index 0917d79..ebebf5d 100644 --- a/load_audio_ui.py +++ b/load_audio_ui.py @@ -61,9 +61,10 @@ class LoadAudioUI: return { "required": { - "audio": (files, {"audio_upload": True}), + "audio": (files, {"audio_upload": True}), # Moved to the top so it appears first "start_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}), "end_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}), + "duration": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}), }, "optional": { "audioUI": ("AUDIO_UI",) @@ -82,7 +83,7 @@ class LoadAudioUI: # where our fallback silence logic can handle the missing file gracefully. return True - def load_audio(self, audio, start_time, end_time, **kwargs): + def load_audio(self, audio, start_time, end_time, duration, **kwargs): # Determine the annotated file path if a file is actually selected # We wrap this in a try/except because get_annotated_filepath can fail if # the input string is malformed or doesn't follow expected paths. diff --git a/pyproject.toml b/pyproject.toml index 382f65b..cceabab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "WhatDreamsCost-ComfyUI" description = "A variety of custom ComfyUI nodes and workflows for creatives." -version = "1.2.4" +version = "1.2.5" license = {file = "LICENSE"} # classifiers = [ # # For OS-independent nodes (works on all operating systems)