-
-
Notifications
You must be signed in to change notification settings - Fork 77
Party mode enhancements #1544
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Party mode enhancements #1544
Changes from all commits
e6e4db4
22964ad
9a84d5b
b22a80f
42a2c0f
1fdbbfb
9608f45
f27905e
6d43be3
dea03fe
b52f922
001f31c
ce35638
99c82d5
2cee62f
5d321ef
c3c7d35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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> | ||||||||||||||||||
|
|
@@ -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"; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||
|
|
@@ -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) { | ||||||||||||||||||
|
|
@@ -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(); | ||||||||||||||||||
|
|
@@ -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(() => { | ||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Suggested change
|
||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| onUnmounted(() => { | ||||||||||||||||||
|
|
@@ -222,6 +234,7 @@ onUnmounted(() => { | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| .qr-link { | ||||||||||||||||||
| position: relative; | ||||||||||||||||||
| display: block; | ||||||||||||||||||
| cursor: pointer; | ||||||||||||||||||
| transition: | ||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||||||||||||||
|
|
||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.