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:
+51
-3
@@ -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",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user