mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 13:56:37 +08:00
bug处理
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/truewhile/MeBox/internal/middleware"
|
"github.com/truewhile/MeBox/internal/middleware"
|
||||||
"github.com/truewhile/MeBox/internal/model"
|
"github.com/truewhile/MeBox/internal/model"
|
||||||
|
"github.com/truewhile/MeBox/internal/repository"
|
||||||
"github.com/truewhile/MeBox/internal/service"
|
"github.com/truewhile/MeBox/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,11 +110,25 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
for _, p := range previews {
|
for _, p := range previews {
|
||||||
out = append(out, webLibraryPayload{Library: p.Library, Total: p.Total, Cards: p.Cards})
|
out = append(out, webLibraryPayload{Library: p.Library, Total: p.Total, Cards: p.Cards})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, l := range libs {
|
visibility := mediaVisibilityForRequest(c, svc)
|
||||||
out = append(out, webLibraryPayload{Library: l})
|
libIDs := make([]string, len(libs))
|
||||||
|
for i, l := range libs {
|
||||||
|
libIDs[i] = l.ID
|
||||||
|
}
|
||||||
|
counts, _ := svc.Repo.Media.CountByLibraries(ctx, libIDs, repository.MediaQueryFilter{
|
||||||
|
IncludeNSFW: visibility.IncludeNSFW,
|
||||||
|
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
|
||||||
|
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
|
||||||
|
})
|
||||||
|
for _, l := range libs {
|
||||||
|
var total int64
|
||||||
|
if counts != nil {
|
||||||
|
total = counts[l.ID]
|
||||||
|
}
|
||||||
|
out = append(out, webLibraryPayload{Library: l, Total: total})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 远程 Emby 挂载库追加在本地库之后(非管理员视图仍受 allowed_library_ids 约束)。
|
// 远程 Emby 挂载库追加在本地库之后(非管理员视图仍受 allowed_library_ids 约束)。
|
||||||
if svc.EmbyRemote != nil {
|
if svc.EmbyRemote != nil {
|
||||||
if views, err := svc.EmbyRemote.RemoteLibraries(ctx); err == nil {
|
if views, err := svc.EmbyRemote.RemoteLibraries(ctx); err == nil {
|
||||||
|
|||||||
+57
-35
@@ -115,14 +115,15 @@ export function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [fetchedLibIds])
|
}, [fetchedLibIds])
|
||||||
|
|
||||||
// 3. 首屏视口媒体库:优先加载轮播图指定的库和前 3 个库
|
// 3. 首屏优先加载:轮播图库 + 媒体库卡片区前 20 个库 + 首屏前 3 个内容行
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sortedLibraries.length === 0) return
|
if (sortedLibraries.length === 0) return
|
||||||
const carouselLibIds = sortedLibraries
|
const carouselLibIds = sortedLibraries
|
||||||
.filter((l) => l.carousel_enabled === true)
|
.filter((l) => l.carousel_enabled === true)
|
||||||
.map((l) => l.id)
|
.map((l) => l.id)
|
||||||
|
const topGridLibIds = sortedLibraries.slice(0, 20).map((l) => l.id)
|
||||||
const topRowLibIds = sortedLibraries.slice(0, 3).map((l) => l.id)
|
const topRowLibIds = sortedLibraries.slice(0, 3).map((l) => l.id)
|
||||||
const initialTargets = Array.from(new Set([...carouselLibIds, ...topRowLibIds]))
|
const initialTargets = Array.from(new Set([...carouselLibIds, ...topGridLibIds, ...topRowLibIds]))
|
||||||
void fetchPreviews(initialTargets)
|
void fetchPreviews(initialTargets)
|
||||||
}, [sortedLibraries, fetchPreviews])
|
}, [sortedLibraries, fetchPreviews])
|
||||||
|
|
||||||
@@ -139,34 +140,63 @@ export function HomePage() {
|
|||||||
void fetchPreviews(currentTargets)
|
void fetchPreviews(currentTargets)
|
||||||
}, [sortedLibraries, visibleTargetCount, fetchPreviews])
|
}, [sortedLibraries, visibleTargetCount, fetchPreviews])
|
||||||
|
|
||||||
// 底部哨兵监听(触底解锁后续媒体库行)
|
// 当前已拉取并确认有内容的媒体库行
|
||||||
|
const visibleLibraries = useMemo(() => {
|
||||||
|
return sortedLibraries
|
||||||
|
.slice(0, visibleTargetCount)
|
||||||
|
.filter((lib) => (libraryData[lib.id]?.cards?.length ?? 0) > 0)
|
||||||
|
}, [sortedLibraries, visibleTargetCount, libraryData])
|
||||||
|
|
||||||
|
const hasMoreLibraries = visibleTargetCount < sortedLibraries.length
|
||||||
|
|
||||||
|
// 底部哨兵监听与滚动双保险(触底解锁后续媒体库行)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sentinel = sentinelRef.current
|
|
||||||
if (!sentinel) return
|
|
||||||
|
|
||||||
const scrollParent = document.getElementById('app-main-scroll')
|
const scrollParent = document.getElementById('app-main-scroll')
|
||||||
const observer = new IntersectionObserver(
|
if (!scrollParent) return
|
||||||
(entries) => {
|
|
||||||
const [entry] = entries
|
|
||||||
if (entry?.isIntersecting) {
|
|
||||||
setVisibleTargetCount((prev) => {
|
|
||||||
if (prev >= sortedLibraries.length) return prev
|
|
||||||
return Math.min(prev + STEP_ROWS, sortedLibraries.length)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
root: scrollParent,
|
|
||||||
rootMargin: '400px 0px',
|
|
||||||
threshold: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
observer.observe(sentinel)
|
const handleCheckBottom = () => {
|
||||||
return () => {
|
const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight
|
||||||
observer.disconnect()
|
if (remaining < 600) {
|
||||||
|
setVisibleTargetCount((prev) => {
|
||||||
|
if (prev >= sortedLibraries.length) return prev
|
||||||
|
return Math.min(prev + STEP_ROWS, sortedLibraries.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [sortedLibraries.length])
|
|
||||||
|
scrollParent.addEventListener('scroll', handleCheckBottom, { passive: true })
|
||||||
|
|
||||||
|
const sentinel = sentinelRef.current
|
||||||
|
let observer: IntersectionObserver | null = null
|
||||||
|
if (sentinel) {
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const [entry] = entries
|
||||||
|
if (entry?.isIntersecting) {
|
||||||
|
setVisibleTargetCount((prev) => {
|
||||||
|
if (prev >= sortedLibraries.length) return prev
|
||||||
|
return Math.min(prev + STEP_ROWS, sortedLibraries.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: scrollParent,
|
||||||
|
rootMargin: '600px 0px',
|
||||||
|
threshold: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
observer.observe(sentinel)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckBottom()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollParent.removeEventListener('scroll', handleCheckBottom)
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visibleLibraries.length, hasMoreLibraries, sortedLibraries.length])
|
||||||
|
|
||||||
// Quick lookup map for libraries
|
// Quick lookup map for libraries
|
||||||
const libraryMap = useMemo(() => {
|
const libraryMap = useMemo(() => {
|
||||||
@@ -221,15 +251,6 @@ export function HomePage() {
|
|||||||
return candidateMedia.slice(0, 10)
|
return candidateMedia.slice(0, 10)
|
||||||
}, [libraries, libraryData])
|
}, [libraries, libraryData])
|
||||||
|
|
||||||
// 当前已拉取并确认有内容的媒体库行
|
|
||||||
const visibleLibraries = useMemo(() => {
|
|
||||||
return sortedLibraries
|
|
||||||
.slice(0, visibleTargetCount)
|
|
||||||
.filter((lib) => (libraryData[lib.id]?.cards?.length ?? 0) > 0)
|
|
||||||
}, [sortedLibraries, visibleTargetCount, libraryData])
|
|
||||||
|
|
||||||
const hasMoreLibraries = visibleTargetCount < sortedLibraries.length
|
|
||||||
|
|
||||||
// 库列表还没回来先展示整页 loading;库为空时再等一下播放记录,
|
// 库列表还没回来先展示整页 loading;库为空时再等一下播放记录,
|
||||||
// 以免在"空站点"和"有观看记录"两个终态之间闪空白。
|
// 以免在"空站点"和"有观看记录"两个终态之间闪空白。
|
||||||
if (librariesLoading || (libraries.length === 0 && historyLoading)) {
|
if (librariesLoading || (libraries.length === 0 && historyLoading)) {
|
||||||
@@ -261,6 +282,7 @@ export function HomePage() {
|
|||||||
libraries={sortedLibraries}
|
libraries={sortedLibraries}
|
||||||
libraryData={libraryData}
|
libraryData={libraryData}
|
||||||
libraryCounts={libraryCounts}
|
libraryCounts={libraryCounts}
|
||||||
|
onNeedPreviews={fetchPreviews}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -366,10 +366,12 @@ export function HomeLibrariesSection({
|
|||||||
libraries,
|
libraries,
|
||||||
libraryData,
|
libraryData,
|
||||||
libraryCounts,
|
libraryCounts,
|
||||||
|
onNeedPreviews,
|
||||||
}: {
|
}: {
|
||||||
libraries: Library[]
|
libraries: Library[]
|
||||||
libraryData?: Record<string, { cards: SeriesCard[]; items: Media[]; total: number }>
|
libraryData?: Record<string, { cards: SeriesCard[]; items: Media[]; total: number }>
|
||||||
libraryCounts: Record<string, number>
|
libraryCounts: Record<string, number>
|
||||||
|
onNeedPreviews?: (ids: string[]) => void
|
||||||
}) {
|
}) {
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
@@ -381,6 +383,11 @@ export function HomeLibrariesSection({
|
|||||||
return libraries.slice(start, start + PAGE_SIZE)
|
return libraries.slice(start, start + PAGE_SIZE)
|
||||||
}, [libraries, effectivePage])
|
}, [libraries, effectivePage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ids = pagedLibraries.map((l) => l.id)
|
||||||
|
onNeedPreviews?.(ids)
|
||||||
|
}, [pagedLibraries, onNeedPreviews])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--app-border)] pb-3">
|
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--app-border)] pb-3">
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export function LibrariesPage() {
|
|||||||
try {
|
try {
|
||||||
const libs = await libraryAPI.list()
|
const libs = await libraryAPI.list()
|
||||||
setLibraries(libs)
|
setLibraries(libs)
|
||||||
// 优先拉取前 3 个库的卡片预览
|
// 优先拉取入口卡片网格当前页(前 20 个库)的预览
|
||||||
const topIds = libs.slice(0, 3).map((l) => l.id)
|
const topIds = libs.slice(0, 20).map((l) => l.id)
|
||||||
void fetchPreviews(topIds)
|
void fetchPreviews(topIds)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|||||||
@@ -128,33 +128,54 @@ export function LibrariesContent({
|
|||||||
onNeedPreviews?.(currentTargets)
|
onNeedPreviews?.(currentTargets)
|
||||||
}, [previews, visibleCount, onNeedPreviews])
|
}, [previews, visibleCount, onNeedPreviews])
|
||||||
|
|
||||||
|
// 底部哨兵监听与滚动双保险(触底解锁后续媒体库货架)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sentinel = sentinelRef.current
|
|
||||||
if (!sentinel) return
|
|
||||||
|
|
||||||
const scrollParent = document.getElementById('app-main-scroll')
|
const scrollParent = document.getElementById('app-main-scroll')
|
||||||
const observer = new IntersectionObserver(
|
if (!scrollParent) return
|
||||||
(entries) => {
|
|
||||||
const [entry] = entries
|
|
||||||
if (entry?.isIntersecting) {
|
|
||||||
setVisibleCount((prev) => {
|
|
||||||
if (prev >= previews.length) return prev
|
|
||||||
return Math.min(prev + STEP_SHELVES, previews.length)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
root: scrollParent,
|
|
||||||
rootMargin: '400px 0px',
|
|
||||||
threshold: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
observer.observe(sentinel)
|
const handleCheckBottom = () => {
|
||||||
return () => {
|
const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight
|
||||||
observer.disconnect()
|
if (remaining < 600) {
|
||||||
|
setVisibleCount((prev) => {
|
||||||
|
if (prev >= previews.length) return prev
|
||||||
|
return Math.min(prev + STEP_SHELVES, previews.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [previews.length])
|
|
||||||
|
scrollParent.addEventListener('scroll', handleCheckBottom, { passive: true })
|
||||||
|
|
||||||
|
const sentinel = sentinelRef.current
|
||||||
|
let observer: IntersectionObserver | null = null
|
||||||
|
if (sentinel) {
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const [entry] = entries
|
||||||
|
if (entry?.isIntersecting) {
|
||||||
|
setVisibleCount((prev) => {
|
||||||
|
if (prev >= previews.length) return prev
|
||||||
|
return Math.min(prev + STEP_SHELVES, previews.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: scrollParent,
|
||||||
|
rootMargin: '600px 0px',
|
||||||
|
threshold: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
observer.observe(sentinel)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckBottom()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollParent.removeEventListener('scroll', handleCheckBottom)
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visibleCount, previews.length])
|
||||||
|
|
||||||
const visiblePreviews = useMemo(() => {
|
const visiblePreviews = useMemo(() => {
|
||||||
return previews.slice(0, visibleCount)
|
return previews.slice(0, visibleCount)
|
||||||
@@ -169,6 +190,11 @@ export function LibrariesContent({
|
|||||||
return previews.slice(start, start + ENTRY_PAGE_SIZE)
|
return previews.slice(start, start + ENTRY_PAGE_SIZE)
|
||||||
}, [previews, effectiveEntryPage])
|
}, [previews, effectiveEntryPage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ids = pagedPreviews.map((p) => p.library.id)
|
||||||
|
onNeedPreviews?.(ids)
|
||||||
|
}, [pagedPreviews, onNeedPreviews])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user