Skip to content

Party mode enhancements#1544

Merged
MarvinSchenkel merged 17 commits intomusic-assistant:mainfrom
apophisnow:party-mode-enhancements
Mar 10, 2026
Merged

Party mode enhancements#1544
MarvinSchenkel merged 17 commits intomusic-assistant:mainfrom
apophisnow:party-mode-enhancements

Conversation

@apophisnow
Copy link
Contributor

This branch is to collect and fix any bugs or enhancements found during beta testing for Party Mode prior to the GA release.

apophisnow and others added 12 commits March 6, 2026 13:09
Track items now require a tap to expand and reveal the Add and Boost
action buttons, resulting in a cleaner search results interface.
Artist items continue to show the View Songs button immediately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Queue items in the guest view can now be tapped to reveal a Boost
button, allowing guests to boost an existing track in the queue to
play sooner. Only upcoming (non-played) items are interactive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user navigates to the party mode dashboard but does not have
access to the configured player, an error message is displayed:
"You do not have access to the configured player. Please contact
the admin."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When scanning a party mode QR code with remote access enabled, the
remote_id and join code from the URL were not being used to auto-login.
The user would see the "enter remote ID" page instead of connecting
automatically.

The issue was in the fallthrough logic: when both remote_id and join
params were present but the combined handler didn't succeed, neither
the individual remote_id handler (required !join) nor the join handler
(required !remote_id) would run, leaving both params unprocessed.

The fix removes the !urlJoinCode guard from the remote_id handler so
it always pre-fills the remote ID and stores any join code in session
storage for the remote-only auto-connect path to pick up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-expand behavior

- Call new boost_queue_item endpoint with queue_item_id instead of
  add_to_queue with URI, which was creating duplicate tracks
- Only one track can be expanded at a time in both search results
  and queue views
- Collapse expanded track when boost button is pressed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make QR code clickable to copy the party URL to clipboard with an
  animated overlay bubble for feedback
- Remove checkRemoteAccessStatus() which called the admin-only
  remote_access/info endpoint, causing "Admin access required" errors
  for regular users on the jukebox dashboard
- Add link_copied translation key

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…timing

- Use translation keys for copy-to-clipboard feedback instead of raw URL
- Use theme CSS variables for copy bubble colors instead of hardcoded values
- Scope cursor:pointer to track items only in search results
- Consume boost token only after successful server response

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@apophisnow apophisnow changed the title WIP: Party mode enhancements Party mode enhancements Mar 9, 2026
@apophisnow apophisnow marked this pull request as ready for review March 9, 2026 18:07
Party mode QR and config composable now listen to CORE_STATE_UPDATED
to detect remote access toggles, keeping PROVIDERS_UPDATED for
provider load/unload detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
apophisnow and others added 3 commits March 10, 2026 11:57
…atches

- Align token consumption strategy to always consume after successful
  server response (addToQueue, boostQueueItem, skipCurrentSong)
- Remove leftover debug watch statements in PartyModeGuestView
- Clear accessError at top of refreshPartyPlayer to avoid duplication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@MarvinSchenkel MarvinSchenkel left a comment

Choose a reason for hiding this comment

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

LGTM, thanks @apophisnow

@MarvinSchenkel MarvinSchenkel merged commit 9f80326 into music-assistant:main Mar 10, 2026
3 checks passed
Comment on lines +201 to +204
unsubscribe = () => {
unsubProviders();
unsubCoreState();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Vue has a built in function to handle unsubscribing before unmount. I believe this should be used in place of this manual unsubscribe assignment.

Then the if statement can be removed from the onUnmounted() event.

Edit:
I've seen Marcel use this method inside the 'onMount' event a few times, but there are more examples of it being used outside on its own. I believe both can be used, so worth giving it a go.

Suggested change
unsubscribe = () => {
unsubProviders();
unsubCoreState();
};
onBeforeUnmount(() => {
unsubProviders();
unsubCoreState();
});

Comment on lines +250 to 287
.copy-bubble {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: rgba(var(--v-theme-surface), 0.9);
color: rgb(var(--v-theme-success));
font-size: 0.9rem;
font-weight: 600;
border-radius: 8px;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}

.copy-toast-enter-active {
transition: all 0.2s ease-out;
}

.copy-toast-leave-active {
transition: all 0.3s ease-in;
}

.copy-toast-enter-from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}

.copy-toast-leave-to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}

.qr-display canvas {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if we should be creating whole components from scratch. @MarvinSchenkel thoughts on this?

font-size: 2rem;
font-weight: 600;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.9);
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe there are variables that can be used here, so when switching dark/light mode they will change with that theme change. Check out /src/styles/styles.css.

Same comment for line 326 below.

Comment on lines +48 to +62
<!-- Boost action (shown when expanded on upcoming items) -->
<div v-if="isExpanded && canBoost" class="queue-item-actions">
<v-btn
variant="elevated"
size="small"
:loading="boosting"
:disabled="boostDisabled"
class="boost-btn"
:style="{ backgroundColor: boostBadgeColor }"
@click.stop="$emit('boost', item)"
>
<v-icon start size="small">mdi-rocket-launch</v-icon>
{{ $t("providers.party_mode.boost") }}
</v-btn>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the visual bug i posted where the boost button was floating outside the queue item is partially due to this DIV sitting outside the 'queue-item-row' DIV above. Moving it inside that and setting max-width on the button should resolve this.

fetchQueueItems(true);
});
unsubscribeFunctions.value = [unsub1, unsub2, unsub3];
Copy link
Contributor

Choose a reason for hiding this comment

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

See earlier comment about 'onBeforeUnmount'.

.access-error-title {
font-size: 1.5rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
Copy link
Contributor

Choose a reason for hiding this comment

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

See earlier comment about using variables.


const boostQueueItem = async (item: QueueItem) => {
if (!boostEnabled.value) {
toast.warning($t("providers.party_mode.boost_disabled"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
toast.warning($t("providers.party_mode.boost_disabled"));
toast.warning($t("providers.party_mode.guest_page.boost_disabled"));

if (rateLimitingEnabled.value && boostTokens.value <= 0) {
const minutesUntilNext = getTimeUntilNextToken();
toast.warning(
$t("providers.party_mode.boost_limit_reached", [minutesUntilNext]),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$t("providers.party_mode.boost_limit_reached", [minutesUntilNext]),
$t("providers.party_mode.guest_page.boost_limit_reached", [minutesUntilNext]),

toast.success($t("providers.party_mode.guest_page.item_boosted", [name]));
} catch (error) {
console.error("Failed to boost queue item:", error);
toast.error($t("providers.party_mode.add_to_queue_failed"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
toast.error($t("providers.party_mode.add_to_queue_failed"));
toast.error($t("providers.party_mode.guest_page.add_to_queue_failed"));

Comment on lines 503 to 507
try {
const partyPlayerId = await api.sendCommand<string | null>(
"party_mode/player",
);
partyModeQueueId.value = partyPlayerId;
if (partyPlayerId && !store.activePlayerId) {
store.activePlayerId = partyPlayerId;
}
console.debug("[GuestView] partyPlayerId:", partyPlayerId);
console.debug("[GuestView] store.activePlayerId:", store.activePlayerId);
console.debug("[GuestView] store.activePlayer:", store.activePlayer);
console.debug("[GuestView] api.players keys:", Object.keys(api.players));
console.debug("[GuestView] api.queues keys:", Object.keys(api.queues));
console.debug("[GuestView] queue state:", queue.currentQueue.value?.state);
await refreshPartyPlayer();
} catch (error) {
console.error("Failed to fetch party mode player:", error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I would move the try catch into the 'refreshPartyPlayer' method, that way its all together as one and always handled when called.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants