Separate scrolling for browse and details page even though they are laid out side by side on the layout (with one hidden out of screen).

This commit is contained in:
2026-05-30 04:00:53 +00:00
parent fe5be2087d
commit b51a7ea73d
3 changed files with 84 additions and 24 deletions
+51 -3
View File
@@ -35,6 +35,7 @@
:mpc-be-connected="mpcBeConnected" :mpc-be-connected="mpcBeConnected"
:nav-row="1" :nav-row="1"
:position="headerPosition" :position="headerPosition"
:style="headerStyle"
@search="updateSearchQuery" @search="updateSearchQuery"
@go-back="goBack" @go-back="goBack"
/> />
@@ -62,7 +63,12 @@
<div v-else class="page-slider"> <div v-else class="page-slider">
<div class="page-slider-track" :class="{ 'is-detail-open': isDetailOpen }"> <div class="page-slider-track" :class="{ 'is-detail-open': isDetailOpen }">
<!-- Browse/Search page (left panel) --> <!-- Browse/Search page (left panel) -->
<main class="main-content page-slider-panel" data-nav-scope="browse"> <main
ref="browsePanelRef"
class="main-content page-slider-panel"
data-nav-scope="browse"
@scroll.passive="handlePanelScroll('browse')"
>
<!-- Movies and Series views - both always rendered for smooth transitions --> <!-- Movies and Series views - both always rendered for smooth transitions -->
<div v-if="!searchQuery" class="view-container"> <div v-if="!searchQuery" class="view-container">
<Transition name="view-zoom" mode="out-in"> <Transition name="view-zoom" mode="out-in">
@@ -152,7 +158,12 @@
</main> </main>
<!-- Detail page (right panel) --> <!-- Detail page (right panel) -->
<main class="main-content page-slider-panel page-slider-detail-panel" data-nav-scope="detail"> <main
ref="detailPanelRef"
class="main-content page-slider-panel page-slider-detail-panel"
data-nav-scope="detail"
@scroll.passive="handlePanelScroll('detail')"
>
<MediaDetail <MediaDetail
v-if="detailItemForRender" v-if="detailItemForRender"
v-show="isDetailOpen" v-show="isDetailOpen"
@@ -317,10 +328,22 @@ const mpcBeConnected = ref(false)
const resumePositions = ref<Record<string, number>>({}) const resumePositions = ref<Record<string, number>>({})
const searchQuery = ref(getRouteSearchQuery()) const searchQuery = ref(getRouteSearchQuery())
const searchReturnPath = ref<string | null>(null) const searchReturnPath = ref<string | null>(null)
const browsePanelRef = ref<HTMLElement | null>(null)
const detailPanelRef = ref<HTMLElement | null>(null)
const browsePanelScrollTop = ref(0)
const detailPanelScrollTop = ref(0)
const MPC_BE_OPENING_GRACE_MS = 4000 const MPC_BE_OPENING_GRACE_MS = 4000
const mpcBeOpeningUntil = ref(0) const mpcBeOpeningUntil = ref(0)
let mpcBePollTimer: number | null = null let mpcBePollTimer: number | null = null
function handlePanelScroll(scope: "browse" | "detail") {
if (scope === "browse") {
browsePanelScrollTop.value = browsePanelRef.value?.scrollTop || 0
return
}
detailPanelScrollTop.value = detailPanelRef.value?.scrollTop || 0
}
function isMpcFamilySelected(): boolean { function isMpcFamilySelected(): boolean {
// Empty port means Web UI is disabled // Empty port means Web UI is disabled
if (settings.playerMpcPort === null) return false if (settings.playerMpcPort === null) return false
@@ -620,6 +643,8 @@ function handleEscapeKey(event: KeyboardEvent) {
onMounted(() => { onMounted(() => {
void refreshPlayerStatus() void refreshPlayerStatus()
void refreshResumePositions() void refreshResumePositions()
browsePanelScrollTop.value = browsePanelRef.value?.scrollTop || 0
detailPanelScrollTop.value = detailPanelRef.value?.scrollTop || 0
setActiveNavigationScope(isDetailOpen.value ? "detail" : "browse") setActiveNavigationScope(isDetailOpen.value ? "detail" : "browse")
document.addEventListener("keydown", handleEscapeKey) document.addEventListener("keydown", handleEscapeKey)
window.addEventListener("mediahive:gamepad-action", onGamepadAction as EventListener) window.addEventListener("mediahive:gamepad-action", onGamepadAction as EventListener)
@@ -707,6 +732,20 @@ const isDetailOpen = computed(() => selectedItem.value !== null)
const lastDetailItem = ref<MediaItem | null>(null) const lastDetailItem = ref<MediaItem | null>(null)
const detailItemForRender = computed(() => selectedItem.value ?? lastDetailItem.value) const detailItemForRender = computed(() => selectedItem.value ?? lastDetailItem.value)
const activePanelScrollTop = computed(() => {
if (loading.value || error.value) return 0
return isDetailOpen.value ? detailPanelScrollTop.value : browsePanelScrollTop.value
})
const headerStyle = computed(() => {
if (route.path === "/settings") {
return {}
}
return {
transform: `translateY(${-activePanelScrollTop.value}px)`,
}
})
watch(selectedItem, (item) => { watch(selectedItem, (item) => {
if (item) { if (item) {
lastDetailItem.value = item lastDetailItem.value = item
@@ -1317,7 +1356,9 @@ async function handleOpenFolder(folderPath: string, explicitRootId?: string | nu
<style scoped> <style scoped>
.app { .app {
height: 100vh;
min-height: 100vh; min-height: 100vh;
overflow: hidden;
} }
/* Scanning progress debug overlay */ /* Scanning progress debug overlay */
@@ -1407,12 +1448,14 @@ async function handleOpenFolder(folderPath: string, explicitRootId?: string | nu
.page-slider { .page-slider {
width: 100vw; width: 100vw;
overflow-x: hidden; height: 100vh;
overflow: hidden;
} }
.page-slider-track { .page-slider-track {
display: flex; display: flex;
width: 200vw; width: 200vw;
height: 100vh;
transition: transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1); transition: transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
will-change: transform; will-change: transform;
} }
@@ -1425,6 +1468,11 @@ async function handleOpenFolder(folderPath: string, explicitRootId?: string | nu
flex: 0 0 100vw; flex: 0 0 100vw;
width: 100vw; width: 100vw;
min-width: 100vw; min-width: 100vw;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
} }
.page-slider-detail-panel { .page-slider-detail-panel {
@@ -387,26 +387,39 @@ function handleSyncedRowResize() {
} }
function ensureElementVisibleVertically(element: HTMLElement) { function ensureElementVisibleVertically(element: HTMLElement) {
const scrollContainer = element.closest<HTMLElement>("[data-nav-scope]")
const viewportRect = scrollContainer?.getBoundingClientRect() || null
const scrollByTop = (delta: number) => {
if (scrollContainer) {
scrollContainer.scrollBy({
top: delta,
behavior: "smooth",
})
return
}
window.scrollBy({
top: delta,
behavior: "smooth",
})
}
if (element.hasAttribute("data-nav-release-item")) { if (element.hasAttribute("data-nav-release-item")) {
const rootStyle = window.getComputedStyle(document.documentElement) const rootStyle = window.getComputedStyle(document.documentElement)
const headerHeight = parseFloat(rootStyle.getPropertyValue("--header-height") || "0") const headerHeight = parseFloat(rootStyle.getPropertyValue("--header-height") || "0")
const topMargin = headerHeight + 24 const topMargin = headerHeight + 24
const bottomMargin = 24 const bottomMargin = 24
const rect = element.getBoundingClientRect() const rect = element.getBoundingClientRect()
const topBoundary = (viewportRect?.top || 0) + topMargin
const bottomBoundary = (viewportRect?.bottom || window.innerHeight) - bottomMargin
if (rect.top < topMargin) { if (rect.top < topBoundary) {
window.scrollBy({ scrollByTop(rect.top - topBoundary)
top: rect.top - topMargin,
behavior: "smooth",
})
return return
} }
if (rect.bottom > window.innerHeight - bottomMargin) { if (rect.bottom > bottomBoundary) {
window.scrollBy({ scrollByTop(rect.bottom - bottomBoundary)
top: rect.bottom - (window.innerHeight - bottomMargin),
behavior: "smooth",
})
} }
return return
} }
@@ -416,20 +429,16 @@ function ensureElementVisibleVertically(element: HTMLElement) {
const topMargin = headerHeight + 24 const topMargin = headerHeight + 24
const bottomMargin = 24 const bottomMargin = 24
const rect = element.getBoundingClientRect() const rect = element.getBoundingClientRect()
const topBoundary = (viewportRect?.top || 0) + topMargin
const bottomBoundary = (viewportRect?.bottom || window.innerHeight) - bottomMargin
if (rect.top < topMargin) { if (rect.top < topBoundary) {
window.scrollBy({ scrollByTop(rect.top - topBoundary)
top: rect.top - topMargin,
behavior: "smooth",
})
return return
} }
if (rect.bottom > window.innerHeight - bottomMargin) { if (rect.bottom > bottomBoundary) {
window.scrollBy({ scrollByTop(rect.bottom - bottomBoundary)
top: rect.bottom - (window.innerHeight - bottomMargin),
behavior: "smooth",
})
} }
} }
+4 -1
View File
@@ -32,9 +32,10 @@ body {
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-primary); background-color: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
height: 100%;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: hidden;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-align: justify; text-align: justify;
hyphens: auto; hyphens: auto;
@@ -54,8 +55,10 @@ html:not(.pointer-visible) * {
} }
#app { #app {
height: 100vh;
min-height: 100vh; min-height: 100vh;
position: relative; position: relative;
overflow: hidden;
} }
/* Scrollbar styling */ /* Scrollbar styling */