Add noise gate, palette quantization filters and drag-and-drop filter ordering
- Add noise gate filter: suppresses per-pixel color flicker below threshold using stateful frame comparison with pre-allocated int16 buffers - Add palette quantization filter: maps pixels to nearest color in preset or custom hex palette, using chunked processing for memory efficiency - Add "string" option type to filter schema system (base, API, frontend) - Replace up/down buttons with pointer-event drag-and-drop in PP template filter list, with clone/placeholder feedback and modal auto-scroll - Add frame_interpolation locale keys (was missing from all 3 locales) - Update TODO.md: mark completed processing pipeline items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1942,12 +1942,11 @@ export function renderModalFilterList() {
|
||||
|
||||
html += `<div class="pp-filter-card${isExpanded ? ' expanded' : ''}" data-filter-index="${index}">
|
||||
<div class="pp-filter-card-header" onclick="toggleFilterExpand(${index})">
|
||||
<span class="pp-filter-drag-handle" title="${t('filters.drag_to_reorder')}">⠇</span>
|
||||
<span class="pp-filter-card-chevron">${isExpanded ? '▼' : '▶'}</span>
|
||||
<span class="pp-filter-card-name">${escapeHtml(filterName)}</span>
|
||||
${summary ? `<span class="pp-filter-card-summary">${escapeHtml(summary)}</span>` : ''}
|
||||
<div class="pp-filter-card-actions" onclick="event.stopPropagation()">
|
||||
<button type="button" class="btn-filter-action" onclick="moveFilter(${index}, -1)" title="${t('filters.move_up')}" ${index === 0 ? 'disabled' : ''}>▲</button>
|
||||
<button type="button" class="btn-filter-action" onclick="moveFilter(${index}, 1)" title="${t('filters.move_down')}" ${index === _modalFilters.length - 1 ? 'disabled' : ''}>▼</button>
|
||||
<button type="button" class="btn-filter-action btn-filter-remove" onclick="removeFilter(${index})" title="${t('filters.remove')}">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1988,6 +1987,14 @@ export function renderModalFilterList() {
|
||||
${options}
|
||||
</select>
|
||||
</div>`;
|
||||
} else if (opt.type === 'string') {
|
||||
const maxLen = opt.max_length || 500;
|
||||
html += `<div class="pp-filter-option">
|
||||
<label for="${inputId}"><span>${escapeHtml(opt.label)}:</span></label>
|
||||
<input type="text" id="${inputId}" value="${escapeHtml(String(currentVal))}"
|
||||
maxlength="${maxLen}" class="pp-filter-text-input"
|
||||
onchange="updateFilterOption(${index}, '${opt.key}', this.value)">
|
||||
</div>`;
|
||||
} else {
|
||||
html += `<div class="pp-filter-option">
|
||||
<label for="${inputId}">
|
||||
@@ -2006,6 +2013,161 @@ export function renderModalFilterList() {
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
_initFilterDrag();
|
||||
}
|
||||
|
||||
/* ── PP filter drag-and-drop reordering ── */
|
||||
|
||||
const _FILTER_DRAG_THRESHOLD = 5;
|
||||
const _FILTER_SCROLL_EDGE = 60;
|
||||
const _FILTER_SCROLL_SPEED = 12;
|
||||
let _filterDragState = null;
|
||||
|
||||
function _initFilterDrag() {
|
||||
const container = document.getElementById('pp-filter-list');
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener('pointerdown', (e) => {
|
||||
const handle = e.target.closest('.pp-filter-drag-handle');
|
||||
if (!handle) return;
|
||||
const card = handle.closest('.pp-filter-card');
|
||||
if (!card) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const fromIndex = parseInt(card.dataset.filterIndex, 10);
|
||||
_filterDragState = {
|
||||
card,
|
||||
container,
|
||||
startY: e.clientY,
|
||||
started: false,
|
||||
clone: null,
|
||||
placeholder: null,
|
||||
offsetY: 0,
|
||||
fromIndex,
|
||||
scrollRaf: null,
|
||||
};
|
||||
|
||||
const onMove = (ev) => _onFilterDragMove(ev);
|
||||
const onUp = () => {
|
||||
document.removeEventListener('pointermove', onMove);
|
||||
document.removeEventListener('pointerup', onUp);
|
||||
_onFilterDragEnd();
|
||||
};
|
||||
document.addEventListener('pointermove', onMove);
|
||||
document.addEventListener('pointerup', onUp);
|
||||
});
|
||||
}
|
||||
|
||||
function _onFilterDragMove(e) {
|
||||
const ds = _filterDragState;
|
||||
if (!ds) return;
|
||||
|
||||
if (!ds.started) {
|
||||
if (Math.abs(e.clientY - ds.startY) < _FILTER_DRAG_THRESHOLD) return;
|
||||
_startFilterDrag(ds, e);
|
||||
}
|
||||
|
||||
// Position clone at pointer
|
||||
ds.clone.style.top = (e.clientY - ds.offsetY) + 'px';
|
||||
|
||||
// Find drop target by vertical midpoint
|
||||
const cards = ds.container.querySelectorAll('.pp-filter-card');
|
||||
for (const card of cards) {
|
||||
if (card.style.display === 'none') continue;
|
||||
const r = card.getBoundingClientRect();
|
||||
if (e.clientY >= r.top && e.clientY <= r.bottom) {
|
||||
const before = e.clientY < r.top + r.height / 2;
|
||||
if (card === ds.lastTarget && before === ds.lastBefore) break;
|
||||
ds.lastTarget = card;
|
||||
ds.lastBefore = before;
|
||||
if (before) {
|
||||
ds.container.insertBefore(ds.placeholder, card);
|
||||
} else {
|
||||
ds.container.insertBefore(ds.placeholder, card.nextSibling);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll near viewport edges
|
||||
_filterAutoScroll(e.clientY, ds);
|
||||
}
|
||||
|
||||
function _startFilterDrag(ds, e) {
|
||||
ds.started = true;
|
||||
const rect = ds.card.getBoundingClientRect();
|
||||
|
||||
// Clone for visual feedback
|
||||
const clone = ds.card.cloneNode(true);
|
||||
clone.className = ds.card.className + ' pp-filter-drag-clone';
|
||||
clone.style.width = rect.width + 'px';
|
||||
clone.style.left = rect.left + 'px';
|
||||
clone.style.top = rect.top + 'px';
|
||||
document.body.appendChild(clone);
|
||||
ds.clone = clone;
|
||||
ds.offsetY = e.clientY - rect.top;
|
||||
|
||||
// Placeholder
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'pp-filter-drag-placeholder';
|
||||
placeholder.style.height = rect.height + 'px';
|
||||
ds.card.parentNode.insertBefore(placeholder, ds.card);
|
||||
ds.placeholder = placeholder;
|
||||
|
||||
// Hide original
|
||||
ds.card.style.display = 'none';
|
||||
document.body.classList.add('pp-filter-dragging');
|
||||
}
|
||||
|
||||
function _onFilterDragEnd() {
|
||||
const ds = _filterDragState;
|
||||
_filterDragState = null;
|
||||
if (!ds || !ds.started) return;
|
||||
|
||||
if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);
|
||||
|
||||
// Determine new index from placeholder position among children
|
||||
let toIndex = 0;
|
||||
for (const child of ds.container.children) {
|
||||
if (child === ds.placeholder) break;
|
||||
if (child.classList.contains('pp-filter-card') && child.style.display !== 'none') {
|
||||
toIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup DOM
|
||||
ds.card.style.display = '';
|
||||
ds.placeholder.remove();
|
||||
ds.clone.remove();
|
||||
document.body.classList.remove('pp-filter-dragging');
|
||||
|
||||
// Reorder _modalFilters array
|
||||
if (toIndex !== ds.fromIndex) {
|
||||
const [item] = _modalFilters.splice(ds.fromIndex, 1);
|
||||
_modalFilters.splice(toIndex, 0, item);
|
||||
renderModalFilterList();
|
||||
_autoGeneratePPTemplateName();
|
||||
}
|
||||
}
|
||||
|
||||
function _filterAutoScroll(clientY, ds) {
|
||||
if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);
|
||||
const modal = ds.container.closest('.modal-body');
|
||||
if (!modal) return;
|
||||
const rect = modal.getBoundingClientRect();
|
||||
let speed = 0;
|
||||
if (clientY < rect.top + _FILTER_SCROLL_EDGE) {
|
||||
speed = -_FILTER_SCROLL_SPEED;
|
||||
} else if (clientY > rect.bottom - _FILTER_SCROLL_EDGE) {
|
||||
speed = _FILTER_SCROLL_SPEED;
|
||||
}
|
||||
if (speed === 0) return;
|
||||
const scroll = () => {
|
||||
modal.scrollTop += speed;
|
||||
ds.scrollRaf = requestAnimationFrame(scroll);
|
||||
};
|
||||
ds.scrollRaf = requestAnimationFrame(scroll);
|
||||
}
|
||||
|
||||
export function addFilterFromSelect() {
|
||||
@@ -2065,6 +2227,8 @@ export function updateFilterOption(filterIndex, optionKey, value) {
|
||||
fi.options[optionKey] = !!value;
|
||||
} else if (optDef && optDef.type === 'select') {
|
||||
fi.options[optionKey] = String(value);
|
||||
} else if (optDef && optDef.type === 'string') {
|
||||
fi.options[optionKey] = String(value);
|
||||
} else if (optDef && optDef.type === 'int') {
|
||||
fi.options[optionKey] = parseInt(value);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user