Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot] 51d0f5010e chore: bump version to 0.0.100 [skip ci] 2026-09-03 12:20:20 +00:00
truewhile b9dd09a5d2 bug处理 2026-09-03 20:19:31 +08:00
6 changed files with 135 additions and 65 deletions
+1 -1
View File
@@ -1 +1 @@
0.0.99
0.0.100
+19 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
@@ -109,11 +110,25 @@ func listLibrariesHandler(svc *service.Container) gin.HandlerFunc {
for _, p := range previews {
out = append(out, webLibraryPayload{Library: p.Library, Total: p.Total, Cards: p.Cards})
}
} else {
for _, l := range libs {
out = append(out, webLibraryPayload{Library: l})
} else {
visibility := mediaVisibilityForRequest(c, svc)
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 约束)。
if svc.EmbyRemote != nil {
if views, err := svc.EmbyRemote.RemoteLibraries(ctx); err == nil {
+57 -35
View File
@@ -115,14 +115,15 @@ export function HomePage() {
}
}, [fetchedLibIds])
// 3. 首屏视口媒体库:优先加载轮播图指定的库和前 3 个库
// 3. 首屏优先加载:轮播图库 + 媒体库卡片区前 20 个库 + 首屏前 3 个内容行
useEffect(() => {
if (sortedLibraries.length === 0) return
const carouselLibIds = sortedLibraries
.filter((l) => l.carousel_enabled === true)
.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 initialTargets = Array.from(new Set([...carouselLibIds, ...topRowLibIds]))
const initialTargets = Array.from(new Set([...carouselLibIds, ...topGridLibIds, ...topRowLibIds]))
void fetchPreviews(initialTargets)
}, [sortedLibraries, fetchPreviews])
@@ -139,34 +140,63 @@ export function HomePage() {
void fetchPreviews(currentTargets)
}, [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(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const scrollParent = document.getElementById('app-main-scroll')
const 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: '400px 0px',
threshold: 0,
},
)
if (!scrollParent) return
observer.observe(sentinel)
return () => {
observer.disconnect()
const handleCheckBottom = () => {
const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight
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
const libraryMap = useMemo(() => {
@@ -221,15 +251,6 @@ export function HomePage() {
return candidateMedia.slice(0, 10)
}, [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;库为空时再等一下播放记录,
// 以免在"空站点"和"有观看记录"两个终态之间闪空白。
if (librariesLoading || (libraries.length === 0 && historyLoading)) {
@@ -261,6 +282,7 @@ export function HomePage() {
libraries={sortedLibraries}
libraryData={libraryData}
libraryCounts={libraryCounts}
onNeedPreviews={fetchPreviews}
/>
)}
+7
View File
@@ -366,10 +366,12 @@ export function HomeLibrariesSection({
libraries,
libraryData,
libraryCounts,
onNeedPreviews,
}: {
libraries: Library[]
libraryData?: Record<string, { cards: SeriesCard[]; items: Media[]; total: number }>
libraryCounts: Record<string, number>
onNeedPreviews?: (ids: string[]) => void
}) {
const PAGE_SIZE = 20
const [currentPage, setCurrentPage] = useState(1)
@@ -381,6 +383,11 @@ export function HomeLibrariesSection({
return libraries.slice(start, start + PAGE_SIZE)
}, [libraries, effectivePage])
useEffect(() => {
const ids = pagedLibraries.map((l) => l.id)
onNeedPreviews?.(ids)
}, [pagedLibraries, onNeedPreviews])
return (
<section className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--app-border)] pb-3">
+2 -2
View File
@@ -62,8 +62,8 @@ export function LibrariesPage() {
try {
const libs = await libraryAPI.list()
setLibraries(libs)
// 优先拉取前 3 个库的卡片预览
const topIds = libs.slice(0, 3).map((l) => l.id)
// 优先拉取入口卡片网格当前页(前 20 个库)的预览
const topIds = libs.slice(0, 20).map((l) => l.id)
void fetchPreviews(topIds)
} finally {
setLoading(false)
+49 -23
View File
@@ -128,33 +128,54 @@ export function LibrariesContent({
onNeedPreviews?.(currentTargets)
}, [previews, visibleCount, onNeedPreviews])
// 底部哨兵监听与滚动双保险(触底解锁后续媒体库货架)
useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const scrollParent = document.getElementById('app-main-scroll')
const 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: '400px 0px',
threshold: 0,
},
)
if (!scrollParent) return
observer.observe(sentinel)
return () => {
observer.disconnect()
const handleCheckBottom = () => {
const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight
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(() => {
return previews.slice(0, visibleCount)
@@ -169,6 +190,11 @@ export function LibrariesContent({
return previews.slice(start, start + ENTRY_PAGE_SIZE)
}, [previews, effectiveEntryPage])
useEffect(() => {
const ids = pagedPreviews.map((p) => p.library.id)
onNeedPreviews?.(ids)
}, [pagedPreviews, onNeedPreviews])
return (
<>
<section className="space-y-4">