mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7f2760728 | |||
| ea732e5bd7 |
@@ -119,6 +119,9 @@ func (p *ProfileService) SetPinnedLibraryIDs(ctx context.Context, userID string,
|
||||
return nil, err
|
||||
}
|
||||
normalized := filterPinnedLibraryIDs(normalizePinnedLibraryIDs(ids), accessible)
|
||||
if normalized == nil {
|
||||
normalized = []string{}
|
||||
}
|
||||
raw, err := json.Marshal(normalized)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -146,6 +149,22 @@ func (p *ProfileService) accessibleLibraryIDSet(ctx context.Context, visibility
|
||||
}
|
||||
out[lib.ID] = struct{}{}
|
||||
}
|
||||
// Mounted Emby libraries are not rows in the local libraries table; their
|
||||
// web IDs are embyremote~{mountID}~{remoteViewID}. Include enabled mounts
|
||||
// from the mount table so pinning them does not get stripped (and so a
|
||||
// pin-save that includes remotes cannot accidentally wipe local pins).
|
||||
if p.repo.EmbyMount != nil {
|
||||
mounts, err := p.repo.EmbyMount.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, mount := range mounts {
|
||||
if !mount.Enabled || strings.TrimSpace(mount.RemoteViewID) == "" {
|
||||
continue
|
||||
}
|
||||
out[EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestProfilePinnedLibrariesFiltersInaccessibleAndPreservesOrder(t *testing.T
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.User{}, &model.Library{}); err != nil {
|
||||
if err := db.AutoMigrate(&model.User{}, &model.Library{}, &model.EmbyMount{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
@@ -59,3 +59,65 @@ func TestProfilePinnedLibrariesFiltersInaccessibleAndPreservesOrder(t *testing.T
|
||||
t.Fatalf("GetPinnedLibraryIDs = %v, want %v", loaded, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilePinnedLibrariesKeepsMountedEmbyAndLocalPins(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.User{}, &model.Library{}, &model.EmbyMount{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
svc := NewProfileService(zap.NewNop(), repos)
|
||||
|
||||
user := &model.User{Username: "viewer", PasswordHash: "hash", Role: "user"}
|
||||
if err := repos.User.Create(t.Context(), user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
local := &model.Library{Name: "Movies", Path: "/media/movies", Type: "movie", Enabled: true}
|
||||
if err := repos.Library.Create(t.Context(), local); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mount := &model.EmbyMount{
|
||||
AccountID: "acct-1",
|
||||
RemoteViewID: "view-42",
|
||||
RemoteViewName: "Remote Movies",
|
||||
Enabled: true,
|
||||
}
|
||||
if err := repos.EmbyMount.Create(t.Context(), mount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteID := EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)
|
||||
disabled := &model.EmbyMount{
|
||||
AccountID: "acct-1",
|
||||
RemoteViewID: "view-99",
|
||||
Enabled: true,
|
||||
}
|
||||
if err := repos.EmbyMount.Create(t.Context(), disabled); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Model(disabled).Update("enabled", false).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
disabledID := EncodeEmbyRemoteID(disabled.ID, disabled.RemoteViewID)
|
||||
|
||||
got, err := svc.SetPinnedLibraryIDs(t.Context(), user.ID, []string{
|
||||
local.ID, remoteID, disabledID, "embyremote~missing~view",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetPinnedLibraryIDs: %v", err)
|
||||
}
|
||||
want := []string{local.ID, remoteID}
|
||||
if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
|
||||
t.Fatalf("SetPinnedLibraryIDs = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
loaded, err := svc.GetPinnedLibraryIDs(t.Context(), user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPinnedLibraryIDs: %v", err)
|
||||
}
|
||||
if len(loaded) != len(want) || loaded[0] != want[0] || loaded[1] != want[1] {
|
||||
t.Fatalf("GetPinnedLibraryIDs = %v, want %v", loaded, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import {
|
||||
loadPinnedLibraryIds,
|
||||
@@ -10,16 +10,26 @@ export function usePinnedLibraries() {
|
||||
const [pinnedIds, setPinnedIds] = useState<string[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [loadError, setLoadError] = useState(false)
|
||||
const loadedRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
setLoadError(false)
|
||||
loadedRef.current = false
|
||||
loadPinnedLibraryIds()
|
||||
.then((ids) => {
|
||||
if (!cancelled) setPinnedIds(ids)
|
||||
if (cancelled) return
|
||||
loadedRef.current = true
|
||||
setPinnedIds(ids)
|
||||
setLoadError(false)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setPinnedIds([])
|
||||
if (cancelled) return
|
||||
loadedRef.current = false
|
||||
setLoadError(true)
|
||||
// Keep any existing pins in memory; never replace a known server list with [].
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
@@ -30,6 +40,11 @@ export function usePinnedLibraries() {
|
||||
}, [])
|
||||
|
||||
const togglePin = useCallback(async (libraryId: string) => {
|
||||
// Avoid saving from an unloaded/failed state — that would overwrite the
|
||||
// server list with only the clicked id and can wipe existing local pins
|
||||
// when mounted Emby ids used to be filtered out server-side.
|
||||
if (!loadedRef.current || loading || loadError) return
|
||||
|
||||
let previous: string[] = []
|
||||
let optimistic: string[] = []
|
||||
setPinnedIds((current) => {
|
||||
@@ -46,7 +61,7 @@ export function usePinnedLibraries() {
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
}, [])
|
||||
}, [loading, loadError])
|
||||
|
||||
return { pinnedIds, loading, syncing, togglePin }
|
||||
return { pinnedIds, loading, syncing, loadError, togglePin }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user