Revert "Keep admitted media frames in one stable dashboard session tree"
This reverts commit 910ed151f1.
This commit is contained in:
@@ -91,8 +91,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stable keyed frames retain playback across late media admission. -->
|
<!-- Panel mode app session — renders alongside current page content -->
|
||||||
<DashboardAppSessions />
|
<Transition name="panel-slide">
|
||||||
|
<div v-if="appLauncher.panelAppId && appLauncher.panelAppId !== appLauncher.mediaAppId" class="app-panel-container">
|
||||||
|
<AppSession
|
||||||
|
v-if="appLauncher.panelAppId !== appLauncher.mediaAppId"
|
||||||
|
:key="appLauncher.panelAppId"
|
||||||
|
:app-id-prop="appLauncher.panelAppId"
|
||||||
|
:path-prop="appLauncher.panelPath ?? undefined"
|
||||||
|
@close="appLauncher.closePanel()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="panel-slide">
|
||||||
|
<div v-if="appLauncher.mediaAppId" v-show="appLauncher.panelAppId === appLauncher.mediaAppId" class="app-panel-container">
|
||||||
|
<AppSession :key="appLauncher.mediaAppId" :app-id-prop="appLauncher.mediaAppId" :suspended="appLauncher.panelAppId !== appLauncher.mediaAppId" @close="appLauncher.closePanel()" />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Persistent Mobile Tabs + Bottom Tab Bar — outside <main> so position:fixed isn't broken by will-change:transform -->
|
<!-- Persistent Mobile Tabs + Bottom Tab Bar — outside <main> so position:fixed isn't broken by will-change:transform -->
|
||||||
@@ -112,7 +127,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||||
import { appConfirm } from '@/composables/useAppConfirm'
|
import { appConfirm } from '@/composables/useAppConfirm'
|
||||||
import DashboardAppSessions from '@/views/dashboard/DashboardAppSessions.vue'
|
import AppSession from '@/views/AppSession.vue'
|
||||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||||
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
|
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<TransitionGroup name="panel-slide">
|
|
||||||
<div v-for="id in sessionIds" :key="id" v-show="launcher.panelAppId === id" class="app-panel-container">
|
|
||||||
<AppSession :app-id-prop="id" :path-prop="paths.get(id)" :suspended="launcher.panelAppId !== id" @close="launcher.closePanel()" />
|
|
||||||
</div>
|
|
||||||
</TransitionGroup>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, reactive, watch } from 'vue'
|
|
||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
|
||||||
import AppSession from '@/views/AppSession.vue'
|
|
||||||
const launcher = useAppLauncherStore()
|
|
||||||
const paths = reactive(new Map<string, string | undefined>())
|
|
||||||
// A single keyed tree owns each frame throughout visible -> retained promotion.
|
|
||||||
const sessionIds = computed(() => [...new Set([launcher.mediaAppId, launcher.panelAppId].filter((id): id is string => !!id))])
|
|
||||||
watch([() => launcher.panelAppId, () => launcher.panelPath], ([id, path]) => {
|
|
||||||
if (!id) return
|
|
||||||
// Reopening playback without an explicit deep link keeps the original URL.
|
|
||||||
if (id === launcher.mediaAppId && paths.has(id) && path == null) return
|
|
||||||
paths.set(id, path ?? undefined)
|
|
||||||
}, { immediate: true })
|
|
||||||
watch(sessionIds, ids => {
|
|
||||||
for (const id of paths.keys()) if (!ids.includes(id)) paths.delete(id)
|
|
||||||
}, { flush: 'sync' })
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.panel-slide-enter-active, .panel-slide-leave-active { transition: none !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
||||||
import { defineComponent, h, nextTick, onMounted, onUnmounted } from 'vue'
|
|
||||||
vi.mock('@/stores/appLauncher', async () => {
|
|
||||||
const {reactive} = await import('vue')
|
|
||||||
const state = reactive({panelAppId:null as string|null, mediaAppId:null as string|null, panelPath:null as string|null,
|
|
||||||
closePanel() {state.panelAppId=null;state.panelPath=null}})
|
|
||||||
return {useAppLauncherStore:()=>state}
|
|
||||||
})
|
|
||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
|
||||||
import DashboardAppSessions from '../DashboardAppSessions.vue'
|
|
||||||
describe('stable dashboard app frame ownership', () => {
|
|
||||||
const mounted=vi.fn(), unmounted=vi.fn()
|
|
||||||
const Session=defineComponent({props:{appIdProp:String,pathProp:String,suspended:Boolean},setup(props) {
|
|
||||||
onMounted(()=>mounted(props.appIdProp));onUnmounted(()=>unmounted(props.appIdProp))
|
|
||||||
return ()=>h('iframe',{'data-app':props.appIdProp,src:props.pathProp ?? '/browse','data-suspended':String(props.suspended)})
|
|
||||||
}})
|
|
||||||
beforeEach(()=> {vi.clearAllMocks();const store=useAppLauncherStore();store.panelAppId=null;store.mediaAppId=null;store.panelPath=null})
|
|
||||||
it('keeps the exact playing frame and deep link through late promotion, close and reopen',async()=>{
|
|
||||||
const store=useAppLauncherStore();store.panelAppId='music';store.panelPath='/browse/album/one'
|
|
||||||
const wrapper=mount(DashboardAppSessions,{global:{stubs:{AppSession:Session,TransitionGroup:false}}})
|
|
||||||
const frame=wrapper.get('iframe').element as HTMLIFrameElement
|
|
||||||
frame.dataset.playbackPosition='17'
|
|
||||||
store.mediaAppId='music';await nextTick()
|
|
||||||
expect(wrapper.get('iframe').element).toBe(frame)
|
|
||||||
expect(mounted).toHaveBeenCalledTimes(1);expect(unmounted).not.toHaveBeenCalled()
|
|
||||||
store.closePanel();await nextTick()
|
|
||||||
expect(wrapper.get('iframe').element).toBe(frame)
|
|
||||||
expect(frame.getAttribute('src')).toBe('/browse/album/one')
|
|
||||||
expect(frame.dataset.suspended).toBe('true')
|
|
||||||
store.panelAppId='music';await nextTick()
|
|
||||||
expect(wrapper.get('iframe').element).toBe(frame)
|
|
||||||
expect(frame.dataset.playbackPosition).toBe('17')
|
|
||||||
expect(frame.getAttribute('src')).toBe('/browse/album/one')
|
|
||||||
expect(frame.dataset.suspended).toBe('false')
|
|
||||||
expect(unmounted).not.toHaveBeenCalled()
|
|
||||||
wrapper.unmount();expect(unmounted).toHaveBeenCalledOnce()
|
|
||||||
})
|
|
||||||
it('keeps retained music while another app opens and promotes that app without recreating it',async()=>{
|
|
||||||
const store=useAppLauncherStore();store.panelAppId='music';store.panelPath='/first'
|
|
||||||
const wrapper=mount(DashboardAppSessions,{global:{stubs:{AppSession:Session,TransitionGroup:false}}})
|
|
||||||
const music=wrapper.get('iframe[data-app="music"]').element
|
|
||||||
store.mediaAppId='music';await nextTick()
|
|
||||||
store.panelPath='/station/two';store.panelAppId='radio';await nextTick()
|
|
||||||
const radio=wrapper.get('iframe[data-app="radio"]').element
|
|
||||||
expect(wrapper.get('iframe[data-app="music"]').element).toBe(music)
|
|
||||||
expect(music.getAttribute('src')).toBe('/first')
|
|
||||||
store.mediaAppId='radio';await nextTick()
|
|
||||||
expect(wrapper.get('iframe[data-app="radio"]').element).toBe(radio)
|
|
||||||
expect(mounted.mock.calls.map(call=>call[0])).toEqual(['music','radio'])
|
|
||||||
expect(unmounted).toHaveBeenCalledWith('music')
|
|
||||||
expect(unmounted).not.toHaveBeenCalledWith('radio')
|
|
||||||
wrapper.unmount()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user