Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions freesound/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,14 @@
# Collections
ENABLE_COLLECTIONS = False
MAX_SOUNDS_PER_COLLECTION = 250
MAX_FEATURED_SOUNDS_PER_COLLECTION = 6
COLLECTION_SORT_OPTIONS = {
"featured": ("Featured first", "featured_order"),
"created_desc": ("Date added (newest first)", "-collectionsound__created"),
"created_asc": ("Date added (oldest first)", "collectionsound__created"),
"name": ("Name", "original_filename"),
}
COLLECTION_SORT_DEFAULT = "featured"

# -------------------------------------------------------------------------------
# Import local settings
Expand Down
199 changes: 130 additions & 69 deletions freesound/static/bw-frontend/src/components/addSoundsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,20 @@ import {
} from '../components/objectSelector';
import { serializedIdListToIntList, combineIdsLists } from '../utils/data';

const handleAddSoundsModal = (
modalId,
modalUrl,
selectedSoundsDestinationElement,
onSoundsSelectedCallback
) => {
handleGenericModal(
modalUrl,
modalContainer => {
const inputElement = modalContainer.getElementsByTagName('input')[0];
inputElement.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
const baseUrl = modalUrl.split('?')[0];
const soundIdsToExclude = combineIdsLists(
serializedIdListToIntList(
selectedSoundsDestinationElement.dataset.selectedIds
),
serializedIdListToIntList(
selectedSoundsDestinationElement.dataset.unselectedIds
)
).join(',');
handleAddSoundsModal(
modalId,
`${baseUrl}?q=${inputElement.value}&exclude=${soundIdsToExclude}`,
selectedSoundsDestinationElement,
onSoundsSelectedCallback
);
}
});

const objectSelectorElement = modalContainer.getElementsByClassName(
'bw-object-selector-container'
)[0];
initializeObjectSelector(objectSelectorElement, element => {
addSelectedSoundsButton.disabled = element.dataset.selectedIds == '';
});

const addSelectedSoundsButton =
modalContainer.getElementsByTagName('button')[0];
addSelectedSoundsButton.disabled = true;
addSelectedSoundsButton.addEventListener('click', evt => {
const selectableSoundElements = [
...modalContainer.getElementsByClassName('bw-selectable-object'),
];
selectableSoundElements.forEach(element => {
const checkbox = element.querySelectorAll('input.bw-checkbox')[0];
if (checkbox.checked) {
const clonedCheckbox = checkbox.cloneNode(); // Cloning the node will remove the event listeners which refer to the "old" sound selector
delete clonedCheckbox.dataset.initialized; // This will force re-initialize the element when added to the new sounds selector
clonedCheckbox.checked = false;
checkbox.parentNode.replaceChild(clonedCheckbox, checkbox);
element.classList.remove('selected');
selectedSoundsDestinationElement.appendChild(element.parentNode);
}
});
onSoundsSelectedCallback(objectSelectorElement.dataset.selectedIds);
dismissModal(modalId);
});
},
undefined,
true,
true
);
// Read the sidecar fields off a server-rendered card so the dynamic flow can
// push freshly-added sounds into the editor's store. ``date_added`` is the
// client-side timestamp; the server stamps its own when the form is saved.
const extractSoundFromCard = card => {
const player = card.querySelector('.bw-player');
if (!player) return null;
const d = player.dataset;
return {
id: parseInt(d.soundId, 10),
name: d.title || '',
username: d.username || '',
duration: parseFloat(d.duration) || 0,
date_added: new Date().toISOString(),
};
};

const prepareAddSoundsModalAndFields = container => {
Expand Down Expand Up @@ -153,18 +103,47 @@ const prepareAddSoundsModalAndFields = container => {

addSoundsButton.addEventListener('click', evt => {
evt.preventDefault();
handleAddSoundsModal(
const getExcludeIds = () =>
combineIdsLists(
serializedIdListToIntList(
selectedSoundsDestinationElement.dataset.selectedIds
),
serializedIdListToIntList(
selectedSoundsDestinationElement.dataset.unselectedIds
)
).join(',');
openAddSoundsModal(
'addSoundsModal',
addSoundsButton.dataset.modalUrl,
selectedSoundsDestinationElement,
selectedSoundIds => {
addSoundsButton.dataset.modalUrl,
getExcludeIds,
modalContainer => {
const objectSelectorElement = modalContainer.getElementsByClassName(
'bw-object-selector-container'
)[0];
const selectableSoundElements = [
...modalContainer.getElementsByClassName('bw-selectable-object'),
];
selectableSoundElements.forEach(element => {
const checkbox = element.querySelectorAll('input.bw-checkbox')[0];
if (checkbox.checked) {
const clonedCheckbox = checkbox.cloneNode();
delete clonedCheckbox.dataset.initialized;
clonedCheckbox.checked = false;
checkbox.parentNode.replaceChild(clonedCheckbox, checkbox);
element.classList.remove('selected');
selectedSoundsDestinationElement.appendChild(element.parentNode);
}
});
const selectedSoundsHiddenInput = document.getElementById(
addSoundsButton.dataset.selectedSoundsHiddenInputId
);
const currentSoundIds = serializedIdListToIntList(
selectedSoundsHiddenInput.value
);
const newSoundIds = serializedIdListToIntList(selectedSoundIds);
const newSoundIds = serializedIdListToIntList(
objectSelectorElement.dataset.selectedIds
);
const combinedIds = combineIdsLists(currentSoundIds, newSoundIds);
selectedSoundsHiddenInput.value = combinedIds.join(',');
if (
Expand All @@ -190,4 +169,86 @@ const prepareAddSoundsModalAndFields = container => {
});
};

export { prepareAddSoundsModalAndFields };
const openAddSoundsModal = (
modalId,
modalUrl,
url,
getExcludeIds,
onSoundsConfirmed
) => {
handleGenericModal(
url,
modalContainer => {
const inputElement = modalContainer.getElementsByTagName('input')[0];
inputElement.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
const baseUrl = modalUrl.split('?')[0];
const excludeIds = getExcludeIds();
openAddSoundsModal(
modalId,
modalUrl,
`${baseUrl}?q=${inputElement.value}&exclude=${excludeIds}`,
getExcludeIds,
onSoundsConfirmed
);
}
});

const objectSelectorElement = modalContainer.getElementsByClassName(
'bw-object-selector-container'
)[0];
const addSelectedSoundsButton =
modalContainer.getElementsByTagName('button')[0];
addSelectedSoundsButton.disabled = true;
initializeObjectSelector(objectSelectorElement, element => {
addSelectedSoundsButton.disabled = element.dataset.selectedIds == '';
});

addSelectedSoundsButton.addEventListener('click', () => {
onSoundsConfirmed(modalContainer);
dismissModal(modalId);
});
},
undefined,
true,
true
);
};

const prepareAddSoundsModalDynamic = (
container,
getExcludeIds,
onSoundsConfirmed
) => {
const addSoundsButton = container.querySelector(
'[data-toggle="add-sounds-modal"]'
);
if (!addSoundsButton) return;

const onConfirmed = modalContainer => {
const sounds = [
...modalContainer.querySelectorAll('.bw-selectable-object'),
].reduce((acc, element) => {
const checkbox = element.querySelector('input.bw-checkbox');
if (checkbox && checkbox.checked) {
acc.push(extractSoundFromCard(element));
}
return acc;
}, []);
onSoundsConfirmed(sounds);
};

addSoundsButton.addEventListener('click', evt => {
evt.preventDefault();
openAddSoundsModal(
'addSoundsModal',
addSoundsButton.dataset.modalUrl,
addSoundsButton.dataset.modalUrl,
getExcludeIds,
onConfirmed
);
});
};

export { prepareAddSoundsModalAndFields, prepareAddSoundsModalDynamic };
3 changes: 3 additions & 0 deletions freesound/static/bw-frontend/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ import './uiThemeDetector';

import { initializeStuffInContainer } from '../utils/initHelper';
initializeStuffInContainer(document, true, true);

// Exposed so htmx-init.js can rehydrate swapped-in subtrees
window.initializeStuffInContainer = initializeStuffInContainer;
73 changes: 71 additions & 2 deletions freesound/static/bw-frontend/src/components/objectSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const initializeObjectSelector = (selectorElement, onChangeCallback) => {
];
selectableObjectElements.forEach(element => {
const checkbox = element.querySelectorAll('input.bw-checkbox')[0];
if (checkbox.dataset.initialized === undefined) {
if (checkbox && checkbox.dataset.initialized === undefined) {
debouncedUpdateObjectSelectorDataProperties(
element.parentNode.parentNode
);
Expand Down Expand Up @@ -91,4 +91,73 @@ const initializeObjectSelector = (selectorElement, onChangeCallback) => {
});
}
};
export { initializeObjectSelector, updateObjectSelectorDataProperties };

// ---------------------------------------------------------------------------
// Visual-state helper for .with-actions containers
// ---------------------------------------------------------------------------
const updateActionUI = (container, actionName, isActive) => {
const btn = container.querySelector('[data-action="' + actionName + '"]');
if (!btn) return;

btn.classList.toggle('active', isActive);

const containerClass = btn.dataset.containerActiveClass;
if (containerClass) {
container.classList.toggle(containerClass, isActive);
}

const activeTitle = btn.dataset.activeTitle;
if (activeTitle) {
if (!btn.dataset.originalTitle) {
btn.dataset.originalTitle = btn.title || '';
}
btn.title = isActive ? activeTitle : btn.dataset.originalTitle;
}

const disables = btn.dataset.disables;
if (disables) {
const targetBtn = container.querySelector(
'[data-action="' + disables + '"]'
);
if (targetBtn) {
targetBtn.disabled = isActive;
}
}

btn.blur();
};

const initializeObjectSelectorActions = (parentElement, store) => {
const containers = parentElement.querySelectorAll(
'.bw-selectable-object.with-actions'
);
containers.forEach(container => {
if (container.dataset.actionsInitialized) return;
container.dataset.actionsInitialized = 'true';

const objectId = parseInt(container.dataset.objectId, 10);

// Restore persisted state for all registered actions
store.actionNames.forEach(name => {
updateActionUI(container, name, store.has(objectId, name));
});

// Bind action buttons identified by data-action attribute
container.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', evt => {
evt.preventDefault();
const nowActive = store.toggleAction(objectId, btn.dataset.action);
if (nowActive !== undefined) {
updateActionUI(container, btn.dataset.action, nowActive);
}
});
});
});
};

export {
initializeObjectSelector,
updateObjectSelectorDataProperties,
initializeObjectSelectorActions,
updateActionUI,
};
22 changes: 22 additions & 0 deletions freesound/static/bw-frontend/src/htmx-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import htmx from 'htmx.org';

window.htmx = htmx;

(function () {
document.body.addEventListener('htmx:configRequest', event => {
event.detail.headers['X-CSRFToken'] =
document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
});

// htmx:load fires for every new subtree added to the DOM by htmx (including after swap).
// Rehydrate players, modals, rating widgets, etc. via the project's generic initializer.
document.body.addEventListener('htmx:load', event => {
if (window.initializeStuffInContainer) {
const newElement = event.detail.elt;
// Skip document.body since it's already initialized on page load by components/index.js
if (newElement && newElement !== document.body) {
window.initializeStuffInContainer(newElement, true, false);
}
}
});
})();
2 changes: 2 additions & 0 deletions freesound/static/bw-frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ import '../styles/index-light.scss';

import './components';
import './utils/polyfills';

import './htmx-init';
Loading
Loading