Skip to content

Commit 9f80326

Browse files
Merge pull request #1544 from apophisnow/party-mode-enhancements
Party mode enhancements
2 parents 2186707 + c3c7d35 commit 9f80326

File tree

12 files changed

+541
-212
lines changed

12 files changed

+541
-212
lines changed

src/components/SendspinPlayer.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ onMounted(() => {
347347
if (!shouldBePlaying || !audioRef.value) return;
348348
349349
audioRef.value.play().catch((error) => {
350-
console.warn("Sendspin: Failed to recover audio element playback:", error);
350+
console.warn(
351+
"Sendspin: Failed to recover audio element playback:",
352+
error,
353+
);
351354
});
352355
});
353356
}

src/components/party-mode/PartyModeQR.vue

Lines changed: 121 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
<Spinner class="size-12" />
55
</div>
66
<div v-else-if="!guestAccessEnabled" class="qr-disabled">
7-
<QrCode :size="64" />
8-
<p>{{ $t("providers.party_mode.guest_access_disabled") }}</p>
7+
<QrCode class="qr-disabled-icon" />
8+
<p class="qr-disabled-title">
9+
{{ $t("providers.party_mode.guest_access_disabled") }}
10+
</p>
911
<p class="qr-hint">{{ $t("providers.party_mode.enable_in_settings") }}</p>
1012
</div>
1113
<div v-else-if="qrCodeUrl" class="qr-display">
12-
<a
13-
:href="qrCodeUrl"
14-
target="_blank"
15-
rel="noopener noreferrer"
16-
class="qr-link"
17-
>
14+
<div class="qr-link" @click="copyUrlToClipboard">
1815
<canvas ref="qrCanvas"></canvas>
19-
</a>
16+
<Transition name="copy-toast">
17+
<div v-if="copyFeedback" class="copy-bubble">
18+
<Check :size="16" />
19+
{{ copyFeedback }}
20+
</div>
21+
</Transition>
22+
</div>
2023
<p v-if="instructionText" class="qr-instructions text-h4">
2124
{{ instructionText }}
2225
</p>
@@ -33,9 +36,10 @@
3336
import { Spinner } from "@/components/ui/spinner";
3437
import { usePartyModeConfig } from "@/composables/usePartyModeConfig";
3538
import api from "@/plugins/api";
36-
import { EventType, RemoteAccessInfo } from "@/plugins/api/interfaces";
39+
import { EventType } from "@/plugins/api/interfaces";
3740
import { $t } from "@/plugins/i18n";
38-
import { AlertCircle, QrCode } from "lucide-vue-next";
41+
import { copyToClipboard } from "@/helpers/utils";
42+
import { AlertCircle, Check, QrCode } from "lucide-vue-next";
3943
import QRCode from "qrcode";
4044
import { onMounted, onUnmounted, ref, watch } from "vue";
4145
@@ -45,8 +49,8 @@ const qrCodeUrl = ref<string>("");
4549
const guestAccessEnabled = ref<boolean>(false);
4650
const loading = ref(true);
4751
const qrSize = ref(320);
52+
const copyFeedback = ref<string>("");
4853
const instructionText = ref($t("providers.party_mode.scan_to_join"));
49-
const lastRemoteAccessEnabled = ref<boolean | null>(null);
5054
const { config: partyConfig, fetchConfig: fetchPartyConfig } =
5155
usePartyModeConfig();
5256
let unsubscribe: (() => void) | null = null;
@@ -62,26 +66,6 @@ const calculateQRSize = () => {
6266
return Math.max(160, Math.min(1024, availableSize));
6367
};
6468
65-
const checkRemoteAccessStatus = async () => {
66-
try {
67-
const info = (await api.sendCommand(
68-
"remote_access/info",
69-
)) as RemoteAccessInfo;
70-
const currentEnabled = info.enabled;
71-
72-
// If remote access status changed, regenerate QR code
73-
if (
74-
lastRemoteAccessEnabled.value !== null &&
75-
lastRemoteAccessEnabled.value !== currentEnabled
76-
) {
77-
await generateQRCode();
78-
}
79-
lastRemoteAccessEnabled.value = currentEnabled;
80-
} catch {
81-
// Ignore errors - remote_access/info may not be available
82-
}
83-
};
84-
8569
const fetchQrConfig = async () => {
8670
const config = await fetchPartyConfig();
8771
if (config) {
@@ -96,6 +80,17 @@ const fetchQrConfig = async () => {
9680
}
9781
};
9882
83+
const copyUrlToClipboard = async () => {
84+
if (!qrCodeUrl.value) return;
85+
const success = await copyToClipboard(qrCodeUrl.value);
86+
copyFeedback.value = success
87+
? $t("providers.party_mode.link_copy_success")
88+
: $t("providers.party_mode.link_copy_fail");
89+
setTimeout(() => {
90+
copyFeedback.value = "";
91+
}, 2000);
92+
};
93+
9994
const renderQRToCanvas = async () => {
10095
if (!qrCanvas.value || !qrCodeUrl.value) return;
10196
qrSize.value = calculateQRSize();
@@ -159,9 +154,6 @@ onMounted(async () => {
159154
await fetchQrConfig();
160155
await generateQRCode();
161156
162-
// Initialize remote access status tracking
163-
await checkRemoteAccessStatus();
164-
165157
// Set up ResizeObserver to regenerate QR code when container size changes
166158
if (qrContainer.value) {
167159
resizeObserver = new ResizeObserver(() => {
@@ -175,21 +167,41 @@ onMounted(async () => {
175167
resizeObserver.observe(qrContainer.value);
176168
}
177169
178-
// Subscribe to PROVIDERS_UPDATED to detect when party_mode or remote_access
179-
// provider is reloaded. Config refresh is handled by the composable automatically.
180-
unsubscribe = api.subscribe(EventType.PROVIDERS_UPDATED, async () => {
181-
await checkRemoteAccessStatus();
170+
// Subscribe to PROVIDERS_UPDATED to detect when party_mode provider is
171+
// loaded/unloaded. Config refresh is handled by the composable automatically.
172+
const unsubProviders = api.subscribe(
173+
EventType.PROVIDERS_UPDATED,
174+
async () => {
175+
const hasPartyMode = Object.values(api.providers).some(
176+
(p) => p.domain === "party_mode",
177+
);
178+
if (hasPartyMode) {
179+
await generateQRCode();
180+
} else {
181+
guestAccessEnabled.value = false;
182+
qrCodeUrl.value = "";
183+
}
184+
},
185+
);
186+
187+
// Subscribe to CORE_STATE_UPDATED to detect when remote access is toggled,
188+
// which changes the party mode join URL between local and remote.
189+
const unsubCoreState = api.subscribe(
190+
EventType.CORE_STATE_UPDATED,
191+
async () => {
192+
const hasPartyMode = Object.values(api.providers).some(
193+
(p) => p.domain === "party_mode",
194+
);
195+
if (hasPartyMode) {
196+
await generateQRCode();
197+
}
198+
},
199+
);
182200
183-
const hasPartyMode = Object.values(api.providers).some(
184-
(p) => p.domain === "party_mode",
185-
);
186-
if (hasPartyMode) {
187-
await generateQRCode();
188-
} else {
189-
guestAccessEnabled.value = false;
190-
qrCodeUrl.value = "";
191-
}
192-
});
201+
unsubscribe = () => {
202+
unsubProviders();
203+
unsubCoreState();
204+
};
193205
});
194206
195207
onUnmounted(() => {
@@ -222,6 +234,7 @@ onUnmounted(() => {
222234
}
223235
224236
.qr-link {
237+
position: relative;
225238
display: block;
226239
cursor: pointer;
227240
transition:
@@ -234,6 +247,43 @@ onUnmounted(() => {
234247
opacity: 0.9;
235248
}
236249
250+
.copy-bubble {
251+
position: absolute;
252+
top: 50%;
253+
left: 50%;
254+
transform: translate(-50%, -50%);
255+
display: flex;
256+
align-items: center;
257+
gap: 6px;
258+
padding: 8px 16px;
259+
background: rgba(var(--v-theme-surface), 0.9);
260+
color: rgb(var(--v-theme-success));
261+
font-size: 0.9rem;
262+
font-weight: 600;
263+
border-radius: 8px;
264+
white-space: nowrap;
265+
pointer-events: none;
266+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
267+
}
268+
269+
.copy-toast-enter-active {
270+
transition: all 0.2s ease-out;
271+
}
272+
273+
.copy-toast-leave-active {
274+
transition: all 0.3s ease-in;
275+
}
276+
277+
.copy-toast-enter-from {
278+
opacity: 0;
279+
transform: translate(-50%, -50%) scale(0.8);
280+
}
281+
282+
.copy-toast-leave-to {
283+
opacity: 0;
284+
transform: translate(-50%, -50%) scale(0.8);
285+
}
286+
237287
.qr-display canvas {
238288
display: block;
239289
border-radius: 8px;
@@ -249,13 +299,32 @@ onUnmounted(() => {
249299
}
250300
251301
.qr-disabled {
302+
display: flex;
303+
flex-direction: column;
304+
align-items: center;
305+
justify-content: center;
252306
text-align: center;
307+
padding: 3rem;
308+
}
309+
310+
.qr-disabled-icon {
311+
width: clamp(64px, 10vw, 120px);
312+
height: clamp(64px, 10vw, 120px);
253313
opacity: 0.5;
314+
margin-bottom: 1.5rem;
254315
}
255316
256-
.qr-disabled p {
257-
margin-top: 0.5rem;
258-
color: rgba(255, 255, 255, 0.5);
317+
.qr-disabled-title {
318+
font-size: 2rem;
319+
font-weight: 600;
320+
margin-bottom: 1rem;
321+
color: rgba(255, 255, 255, 0.9);
322+
}
323+
324+
.qr-disabled .qr-hint {
325+
font-size: 1.25rem;
326+
color: rgba(255, 255, 255, 0.7);
327+
max-width: 500px;
259328
}
260329
261330
.qr-error {

0 commit comments

Comments
 (0)