Skip to content
Open
Show file tree
Hide file tree
Changes from 21 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
10 changes: 9 additions & 1 deletion freesound/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1448,8 +1448,16 @@

# -------------------------------------------------------------------------------
# Collections
ENABLE_COLLECTIONS = False
ENABLE_COLLECTIONS = True
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still be False, you can overwrite in your local_settings.py fie

MAX_SOUNDS_PER_COLLECTION = 250
MAX_FEATURED_SOUNDS_PER_COLLECTION = 250
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we set a lower limit here? Say 10?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the 6 because its 2 full rows on desktop and 3 on mobile but 12 is also good for this manner

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
188 changes: 117 additions & 71 deletions freesound/static/bw-frontend/src/components/addSoundsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,7 @@ import {
updateObjectSelectorDataProperties,
} 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
);
};
import { extractSoundFromCard } from '../utils/soundCard';

const prepareAddSoundsModalAndFields = container => {
const addSoundsButtons = [
Expand Down Expand Up @@ -153,18 +88,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 +154,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 };
77 changes: 75 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,77 @@ 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.actions().forEach(function (entry) {
updateActionUI(
container,
entry.actionName,
store.hasFlag(objectId, entry.flag)
);
});

// 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,
};
32 changes: 32 additions & 0 deletions freesound/static/bw-frontend/src/pages/collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SoundStateStore } from '../utils/soundStateStore';
import { SoundGridEditor } from '../utils/soundGridEditor';

// ─── Data ────────────────────────────────────────────────────

const soundsData = JSON.parse(
document.getElementById('sounds-data').textContent
);
const initialFeaturedIds = JSON.parse(
document.getElementById('featured-data').textContent
);

// ─── State & grid ────────────────────────────────────────────

const store = new SoundStateStore(['featured']).load(soundsData, {
featured: initialFeaturedIds,
});

const editor = new SoundGridEditor({
store,
searchInput: document.getElementById('collection-search'),
syncUrl: true,
renderCard(sound, clone) {
if (store.hasFlag(sound.id, store.FLAG.FEATURED)) {
const col = clone.querySelector('.col-md-4');
if (col) col.classList.add('featured-highlight');
}
return clone;
},
});

editor.renderPage();
Loading
Loading