Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/components/SendspinPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,10 @@ onMounted(() => {
if (!shouldBePlaying || !audioRef.value) return;

audioRef.value.play().catch((error) => {
console.warn("Sendspin: Failed to recover audio element playback:", error);
console.warn(
"Sendspin: Failed to recover audio element playback:",
error,
);
});
});
}
Expand Down
173 changes: 121 additions & 52 deletions src/components/party-mode/PartyModeQR.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
<Spinner class="size-12" />
</div>
<div v-else-if="!guestAccessEnabled" class="qr-disabled">
<QrCode :size="64" />
<p>{{ $t("providers.party_mode.guest_access_disabled") }}</p>
<QrCode class="qr-disabled-icon" />
<p class="qr-disabled-title">
{{ $t("providers.party_mode.guest_access_disabled") }}
</p>
<p class="qr-hint">{{ $t("providers.party_mode.enable_in_settings") }}</p>
</div>
<div v-else-if="qrCodeUrl" class="qr-display">
<a
:href="qrCodeUrl"
target="_blank"
rel="noopener noreferrer"
class="qr-link"
>
<div class="qr-link" @click="copyUrlToClipboard">
<canvas ref="qrCanvas"></canvas>
</a>
<Transition name="copy-toast">
<div v-if="copyFeedback" class="copy-bubble">
<Check :size="16" />
{{ copyFeedback }}
</div>
</Transition>
</div>
<p v-if="instructionText" class="qr-instructions text-h4">
{{ instructionText }}
</p>
Expand All @@ -33,9 +36,10 @@
import { Spinner } from "@/components/ui/spinner";
import { usePartyModeConfig } from "@/composables/usePartyModeConfig";
import api from "@/plugins/api";
import { EventType, RemoteAccessInfo } from "@/plugins/api/interfaces";
import { EventType } from "@/plugins/api/interfaces";
import { $t } from "@/plugins/i18n";
import { AlertCircle, QrCode } from "lucide-vue-next";
import { copyToClipboard } from "@/helpers/utils";
import { AlertCircle, Check, QrCode } from "lucide-vue-next";
import QRCode from "qrcode";
import { onMounted, onUnmounted, ref, watch } from "vue";

Expand All @@ -45,8 +49,8 @@ const qrCodeUrl = ref<string>("");
const guestAccessEnabled = ref<boolean>(false);
const loading = ref(true);
const qrSize = ref(320);
const copyFeedback = ref<string>("");
const instructionText = ref($t("providers.party_mode.scan_to_join"));
const lastRemoteAccessEnabled = ref<boolean | null>(null);
const { config: partyConfig, fetchConfig: fetchPartyConfig } =
usePartyModeConfig();
let unsubscribe: (() => void) | null = null;
Expand All @@ -62,26 +66,6 @@ const calculateQRSize = () => {
return Math.max(160, Math.min(1024, availableSize));
};

const checkRemoteAccessStatus = async () => {
try {
const info = (await api.sendCommand(
"remote_access/info",
)) as RemoteAccessInfo;
const currentEnabled = info.enabled;

// If remote access status changed, regenerate QR code
if (
lastRemoteAccessEnabled.value !== null &&
lastRemoteAccessEnabled.value !== currentEnabled
) {
await generateQRCode();
}
lastRemoteAccessEnabled.value = currentEnabled;
} catch {
// Ignore errors - remote_access/info may not be available
}
};

const fetchQrConfig = async () => {
const config = await fetchPartyConfig();
if (config) {
Expand All @@ -96,6 +80,17 @@ const fetchQrConfig = async () => {
}
};

const copyUrlToClipboard = async () => {
if (!qrCodeUrl.value) return;
const success = await copyToClipboard(qrCodeUrl.value);
copyFeedback.value = success
? $t("providers.party_mode.link_copy_success")
: $t("providers.party_mode.link_copy_fail");
setTimeout(() => {
copyFeedback.value = "";
}, 2000);
};

const renderQRToCanvas = async () => {
if (!qrCanvas.value || !qrCodeUrl.value) return;
qrSize.value = calculateQRSize();
Expand Down Expand Up @@ -159,9 +154,6 @@ onMounted(async () => {
await fetchQrConfig();
await generateQRCode();

// Initialize remote access status tracking
await checkRemoteAccessStatus();

// Set up ResizeObserver to regenerate QR code when container size changes
if (qrContainer.value) {
resizeObserver = new ResizeObserver(() => {
Expand All @@ -175,21 +167,41 @@ onMounted(async () => {
resizeObserver.observe(qrContainer.value);
}

// Subscribe to PROVIDERS_UPDATED to detect when party_mode or remote_access
// provider is reloaded. Config refresh is handled by the composable automatically.
unsubscribe = api.subscribe(EventType.PROVIDERS_UPDATED, async () => {
await checkRemoteAccessStatus();
// Subscribe to PROVIDERS_UPDATED to detect when party_mode provider is
// loaded/unloaded. Config refresh is handled by the composable automatically.
const unsubProviders = api.subscribe(
EventType.PROVIDERS_UPDATED,
async () => {
const hasPartyMode = Object.values(api.providers).some(
(p) => p.domain === "party_mode",
);
if (hasPartyMode) {
await generateQRCode();
} else {
guestAccessEnabled.value = false;
qrCodeUrl.value = "";
}
},
);

// Subscribe to CORE_STATE_UPDATED to detect when remote access is toggled,
// which changes the party mode join URL between local and remote.
const unsubCoreState = api.subscribe(
EventType.CORE_STATE_UPDATED,
async () => {
const hasPartyMode = Object.values(api.providers).some(
(p) => p.domain === "party_mode",
);
if (hasPartyMode) {
await generateQRCode();
}
},
);

const hasPartyMode = Object.values(api.providers).some(
(p) => p.domain === "party_mode",
);
if (hasPartyMode) {
await generateQRCode();
} else {
guestAccessEnabled.value = false;
qrCodeUrl.value = "";
}
});
unsubscribe = () => {
unsubProviders();
unsubCoreState();
};
Comment on lines +201 to +204
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();
});

});

onUnmounted(() => {
Expand Down Expand Up @@ -222,6 +234,7 @@ onUnmounted(() => {
}

.qr-link {
position: relative;
display: block;
cursor: pointer;
transition:
Expand All @@ -234,6 +247,43 @@ onUnmounted(() => {
opacity: 0.9;
}

.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 {
Comment on lines +250 to 287
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?

display: block;
border-radius: 8px;
Expand All @@ -249,13 +299,32 @@ onUnmounted(() => {
}

.qr-disabled {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 3rem;
}

.qr-disabled-icon {
width: clamp(64px, 10vw, 120px);
height: clamp(64px, 10vw, 120px);
opacity: 0.5;
margin-bottom: 1.5rem;
}

.qr-disabled p {
margin-top: 0.5rem;
color: rgba(255, 255, 255, 0.5);
.qr-disabled-title {
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.

}

.qr-disabled .qr-hint {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.7);
max-width: 500px;
}

.qr-error {
Expand Down
Loading
Loading