diff --git a/neode-ui/public/assets/img/companion-phone-screen.webp b/neode-ui/public/assets/img/companion-phone-screen.webp new file mode 100644 index 00000000..a8153380 Binary files /dev/null and b/neode-ui/public/assets/img/companion-phone-screen.webp differ diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index 5252b090..80730a43 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -59,6 +59,7 @@ class="flex-1 inline-flex items-center justify-center rounded-lg bg-orange-500/20 border border-orange-500/30 px-3 py-2.5 text-sm font-medium text-orange-400 hover:bg-orange-500/30 transition-colors text-center" target="_blank" rel="noopener noreferrer" + download > Download app @@ -132,6 +133,7 @@ import { ref, onMounted, onUnmounted, watch } from 'vue' import * as QRCode from 'qrcode' import { IS_DEMO, DEMO_PASSWORD } from '@/composables/useDemoIntro' +import { companionIntroRequested } from '@/composables/useCompanionIntro' import { useLoginTransitionStore } from '@/stores/loginTransition' import { rpcClient } from '@/api/rpc-client' @@ -202,6 +204,18 @@ function maybeShow() { }, 250) } +// Manual open (App Store banner etc.) — ignores the once-per-browser gate. +watch(companionIntroRequested, (requested) => { + if (!requested) return + companionIntroRequested.value = false + if (calmTicker) { + clearInterval(calmTicker) + calmTicker = null + } + step.value = 'download' + visible.value = true +}) + watch(visible, async (isVisible) => { if (!isVisible) return qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, { diff --git a/neode-ui/src/composables/useCompanionIntro.ts b/neode-ui/src/composables/useCompanionIntro.ts new file mode 100644 index 00000000..b05867c0 --- /dev/null +++ b/neode-ui/src/composables/useCompanionIntro.ts @@ -0,0 +1,13 @@ +import { ref } from 'vue' + +/** + * Cross-view trigger for the Remote Companion intro/pairing modal + * (CompanionIntroOverlay, mounted once in Dashboard.vue). Views like the + * App Store banner call openCompanionIntro() to pop it on demand — this + * bypasses the once-per-browser auto-show gate. + */ +export const companionIntroRequested = ref(false) + +export function openCompanionIntro(): void { + companionIntroRequested.value = true +} diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue index e23b6b5f..d26bf257 100644 --- a/neode-ui/src/views/Discover.vue +++ b/neode-ui/src/views/Discover.vue @@ -150,6 +150,9 @@ + + +
all @@ -239,6 +242,7 @@ import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout import { APP_STORE_SECTIONS } from './appStoreCategories' import DiscoverHero from './discover/DiscoverHero.vue' import FeaturedApps from './discover/FeaturedApps.vue' +import CompanionBanner from './discover/CompanionBanner.vue' import AppGrid from './discover/AppGrid.vue' import InstallVersionModal from '@/components/InstallVersionModal.vue' import type { MarketplaceApp, FeaturedApp } from './discover/types' diff --git a/neode-ui/src/views/discover/CompanionBanner.vue b/neode-ui/src/views/discover/CompanionBanner.vue new file mode 100644 index 00000000..57a852b0 --- /dev/null +++ b/neode-ui/src/views/discover/CompanionBanner.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/neode-ui/vite.config.ts b/neode-ui/vite.config.ts index bb1b14eb..3606f9fa 100644 --- a/neode-ui/vite.config.ts +++ b/neode-ui/vite.config.ts @@ -37,7 +37,9 @@ export default defineConfig({ ] }, workbox: { - navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//], + // /packages/ must bypass the SPA fallback — otherwise clicking the + // companion APK download link gets index.html instead of the file. + navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//, /^\/packages\//], cleanupOutdatedCaches: true, globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,mp4,webp}'], globIgnores: [