Revert "Keep admitted media frames in one stable dashboard session tree"

This reverts commit 910ed151f1.
This commit is contained in:
archipelago
2026-10-07 05:37:12 -04:00
parent 910ed151f1
commit 76d4c5c9a2
3 changed files with 18 additions and 88 deletions
+18 -3
View File
@@ -91,8 +91,23 @@
</div>
</div>
<!-- Stable keyed frames retain playback across late media admission. -->
<DashboardAppSessions />
<!-- Panel mode app session — renders alongside current page content -->
<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>
<!-- 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 { useAppLauncherStore } from '../stores/appLauncher'
import { appConfirm } from '@/composables/useAppConfirm'
import DashboardAppSessions from '@/views/dashboard/DashboardAppSessions.vue'
import AppSession from '@/views/AppSession.vue'
import { useLoginTransitionStore } from '../stores/loginTransition'
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()
})
})