Skip to content

Featured sounds#2075

Draft
5aola wants to merge 15 commits intoMTG:masterfrom
5aola:featured-sounds
Draft

Featured sounds#2075
5aola wants to merge 15 commits intoMTG:masterfrom
5aola:featured-sounds

Conversation

@5aola
Copy link
Contributor

@5aola 5aola commented Mar 23, 2026

Collections improvements: featured sounds, client-side grid

Collections model & backend (fscollections/)

  • views.py — models.pycollection() and edit_collection() now serialize all sounds as JSON for client-side rendering (serialize_collection_sounds()), replacing server-side pagination. The edit view switched from a single collection_sounds field to a delta-based approach (added/removed/featured). Cleaned up redundant code and simplified permission logic.

  • forms.py — New CommaSeparatedIdField replacing manual regex parsing. CollectionEditForm now uses three fields: added_sounds, removed_sounds, featured_sounds (delta-based save). save() uses bulk_create with ignore_conflicts and a DB transaction.atomic() block. SelectCollectionOrNewCollectionForm.save() now supports mark_as_featured. Fixed bug in MaintainerForm.clean() (.replace on list -> proper .strip() per item).

  • tests.py

    • test_edit_featured_sounds_as_user — verifies adding sounds with featured flags and clearing featured sounds
    • test_edit_featured_sounds_as_maintainer — verifies maintainers cannot change featured sounds (preserved from owner's original value)
    • test_add_sounds_outside_collection_to_featured — verifies that featuring a sound not in the collection is silently filtered out
    • test_remove_sounds_that_are_featured — verifies removing a sound from the collection also removes it from featured_sound_ids

Frontend — new client-side sound grid system

  • soundStateStore.js — New SoundStateStore class: manages an in-memory map of sounds with per-sound boolean flags (featured, added, remove). Supports add(), remove(), toggle(), hasFlag(), idsWithFlag(), and an onChange listener system.

  • soundGridEditor.js — New SoundGridEditor class: client-side paginated grid with search filtering, multi-column sorting, URL sync (?s=, ?q=, ?page=), and a pluggable renderCard hook. Handles player initialization, rating widgets, modal bindings, and pagination rendering after each page change.

  • soundCard.jspopulateSoundCard() clones a <template> and fills in all sound card fields (player data attributes, links, date, description, rating stars, stat icons). addEditActions() wraps a card with featured-toggle and remove-toggle action buttons for edit mode.

  • formatters.js — Utility functions (escapeAttr, truncate, formatDate, formatNumber, soundPlayerUrls) used by the card renderer.

Frontend — page entry points

  • collection.js — Read-only collection page: loads sounds JSON, creates SoundStateStore + SoundGridEditor with featured highlighting and URL-synced search/sort/pagination.

  • collectionEdit.js — Edit page: same grid but with action buttons (featured toggle, remove toggle). Wires add-sounds modal to dynamically insert new sounds into the store. On form submit, populates hidden added_sounds, removed_sounds, and featured_sounds inputs from store state.

Frontend — updated components

  • objectSelector.js — New initializeObjectSelectorActions() function and updateActionUI() helper for toggle buttons (featured/remove) with visual states, disable-chaining (remove disables featured), and store integration.

  • addSoundsModal.js — New prepareAddSoundsModalDynamic() that works with the client-side grid: excludes already-present sound IDs and feeds selected sounds back into the store, alongside the existing prepareAddSoundsModalAndFields().

Templates

  • collection.html — Replaced server-side sound loop + paginator with a #sounds-grid / #sounds-pagination container, search input, sort dropdown, and JSON script blocks.

  • edit_collection.html — Same client-side grid approach. Form now has hidden fields for added_sounds, removed_sounds, featured_sounds. Buttons are type="button" to prevent accidental submits. Added search and sort controls.

  • display_sound_with_actions.html — New template for sound cards with featured-toggle and remove-toggle action buttons.

  • object_selector.html — Added show_actions branch to render display_sound_small_with_actions instead of the selectable variant.

  • _sound_card_template.html — HTML <template> used by populateSoundCard() for client-side card rendering.

Styles (SCSS)

  • selectableObject.scss — Major addition: styles for .with-actions sound cards including .bw-object-actions button bar, .featured-toggle (yellow active state, hover-to-unfeature), .remove-toggle (red hover, undo state), and .marked-for-removal (dims the card and disables pointer events).

  • inputs.scss, selects.scss, forms.scss — Minor fixes to keep search inputs, selects, and pill-style wrappers consistent when used inside .bw-form containers (no-icon padding variant, nowrap on select options, margin/border resets for embedded search inputs).

  • grid-compat.scss — Added missing offset-* and order-* grid helpers that were removed in an upstream Bootstrap update.

How the client-side collection grid works

The Django view serializes all collection sounds into a JSON array via json_script. On load, this is parsed into a SoundStateStore — an in-memory map of sound objects with per-sound boolean flags (featured, added, remove).

SoundGridEditor consumes the store: filters by search, sorts by the active column, slices to the current page size, then renders each sound by cloning an HTML <template> and populating it via populateSoundCard() (player data-attributes, links, icons, rating widget). After DOM insertion it initializes players, modals, and ratings for that page. Pagination is built client-side from the filtered result count — clicking a page just re-slices and re-renders, no server request.

In edit mode, cards get featured/remove toggle buttons (addEditActions()). Clicks call store.toggle(id, flag), which updates the flag and fires onChange — the grid re-renders or updates the count accordingly.

The add-sounds modal (prepareAddSoundsModalDynamic()) receives the current store IDs as an exclusion list. When the modal opens, handleGenericModal fetches the modal URL via XHR and injects the server-rendered HTML into #genericModalWrapper — so the modal has its own fresh DOM of search result sound cards, entirely separate from the page's sounds grid. On confirm, extractSoundFromModal reads sound metadata directly from the data-* attributes on each selected .bw-player element in the modal DOM, then calls store.add(id, data) for each — the store listener triggers a grid re-render so new cards appear instantly.

On form submit, three hidden inputs are populated from store state: added_sounds, removed_sounds, featured_sounds. The backend applies this delta atomically in CollectionEditForm.save().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant