mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
Add library covers and configurable footer visibility
This commit is contained in:
@@ -99,8 +99,11 @@ export const libraryAPI = {
|
||||
create: (name: string, path: string, type: string) =>
|
||||
api.post<Library>('/libraries', { name, path, type }).then((r) => r.data),
|
||||
|
||||
createWithRoots: (name: string, type: string, roots: LibraryRootInput[]) =>
|
||||
api.post<Library>('/libraries', { name, type, roots }).then((r) => r.data),
|
||||
createWithRoots: (name: string, type: string, roots: LibraryRootInput[], coverURL = '') =>
|
||||
api.post<Library>('/libraries', { name, type, roots, cover_url: coverURL }).then((r) => r.data),
|
||||
|
||||
update: (id: string, payload: { cover_url: string }) =>
|
||||
api.patch<Library>(`/libraries/${id}`, payload).then((r) => r.data),
|
||||
|
||||
remove: (id: string) => api.delete(`/libraries/${id}`).then((r) => r.data),
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { api } from './client'
|
||||
|
||||
export interface PublicUIConfig {
|
||||
hide_community_links_for_users: boolean
|
||||
}
|
||||
|
||||
export const publicAPI = {
|
||||
uiConfig: () => api.get<PublicUIConfig>('/public/ui-config').then((response) => response.data),
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Github, Globe, Send } from 'lucide-react'
|
||||
|
||||
import { publicAPI } from '../api/public'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const LINKS = [
|
||||
{ href: 'https://github.com/ShukeBta/MediaStationGo', icon: Github, label: '开源仓库' },
|
||||
{ href: 'https://github.com/ShukeBta', icon: Globe, label: '作者主页' },
|
||||
@@ -7,6 +11,14 @@ const LINKS = [
|
||||
]
|
||||
|
||||
export function AppFooter({ className = '' }: { className?: string }) {
|
||||
const role = useAuthStore((state) => state.user?.role)
|
||||
const [hideForUsers, setHideForUsers] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
publicAPI.uiConfig().then((config) => setHideForUsers(config.hide_community_links_for_users)).catch(() => undefined)
|
||||
}, [])
|
||||
|
||||
if (hideForUsers && role !== 'admin') return null
|
||||
return (
|
||||
<footer className={`flex items-center justify-center gap-1 ${className}`}>
|
||||
{LINKS.map((link, i) => (
|
||||
|
||||
@@ -10,9 +10,11 @@ export function AdminLibraryPanel() {
|
||||
<AdminLibraryCreateForm
|
||||
name={createForm.name}
|
||||
type={createForm.type}
|
||||
coverURL={createForm.coverURL}
|
||||
roots={createForm.roots}
|
||||
onNameChange={createForm.setName}
|
||||
onTypeChange={createForm.setType}
|
||||
onCoverURLChange={createForm.setCoverURL}
|
||||
onRootChange={createForm.updateRoot}
|
||||
onAddRoot={createForm.addRoot}
|
||||
onRemoveRoot={createForm.removeRoot}
|
||||
@@ -28,6 +30,8 @@ export function AdminLibraryPanel() {
|
||||
onRemoveRoot={rootActions.removeLibraryRoot}
|
||||
onScanLibrary={libraryActions.scanLibrary}
|
||||
onRemoveLibrary={libraryActions.removeLibrary}
|
||||
onAddLibraryRoot={libraryActions.addLibraryRoot}
|
||||
onEditLibraryCover={libraryActions.editLibraryCover}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,9 +6,11 @@ import type { RootDraft } from './adminLibraryPanelModel'
|
||||
type CreateFormProps = {
|
||||
name: string
|
||||
type: string
|
||||
coverURL: string
|
||||
roots: RootDraft[]
|
||||
onNameChange: (value: string) => void
|
||||
onTypeChange: (value: string) => void
|
||||
onCoverURLChange: (value: string) => void
|
||||
onRootChange: (index: number, patch: Partial<RootDraft>) => void
|
||||
onAddRoot: () => void
|
||||
onRemoveRoot: (index: number) => void
|
||||
@@ -18,9 +20,11 @@ type CreateFormProps = {
|
||||
export function AdminLibraryCreateForm({
|
||||
name,
|
||||
type,
|
||||
coverURL,
|
||||
roots,
|
||||
onNameChange,
|
||||
onTypeChange,
|
||||
onCoverURLChange,
|
||||
onRootChange,
|
||||
onAddRoot,
|
||||
onRemoveRoot,
|
||||
@@ -42,6 +46,12 @@ export function AdminLibraryCreateForm({
|
||||
<option value="anime">动漫</option>
|
||||
<option value="music">音乐</option>
|
||||
</select>
|
||||
<input
|
||||
className="input-base md:col-span-2"
|
||||
placeholder="自定义封面 URL(可选)"
|
||||
value={coverURL}
|
||||
onChange={(e) => onCoverURLChange(e.target.value)}
|
||||
/>
|
||||
<div className="md:col-span-4 space-y-2">
|
||||
{roots.map((root, index) => (
|
||||
<CreateRootRow
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MouseEvent, ReactNode } from 'react'
|
||||
import { MoreVertical, Power, PowerOff, RefreshCw, Save, Trash2 } from 'lucide-react'
|
||||
import { Image, MoreVertical, Plus, Power, PowerOff, RefreshCw, Save, Trash2 } from 'lucide-react'
|
||||
|
||||
import type { Library, LibraryRoot } from '../types'
|
||||
import type { RootDraft } from './adminLibraryPanelModel'
|
||||
@@ -15,6 +15,8 @@ type LibraryTableProps = {
|
||||
onRemoveRoot: (library: Library, root: LibraryRoot) => void
|
||||
onScanLibrary: (library: Library) => void
|
||||
onRemoveLibrary: (library: Library) => void
|
||||
onAddLibraryRoot: (library: Library) => void
|
||||
onEditLibraryCover: (library: Library) => void
|
||||
}
|
||||
|
||||
export function AdminLibraryTable({ libs, ...actions }: LibraryTableProps) {
|
||||
@@ -46,7 +48,12 @@ type LibraryTableRowProps = Omit<LibraryTableProps, 'libs'> & {
|
||||
function LibraryTableRow({ library, ...actions }: LibraryTableRowProps) {
|
||||
return (
|
||||
<tr className="border-t border-gray-200">
|
||||
<td className="py-2 pr-3 font-medium text-ink-600">{library.name}</td>
|
||||
<td className="py-2 pr-3 font-medium text-ink-600">
|
||||
<div className="flex items-center gap-2">
|
||||
{library.cover_url && <img src={library.cover_url} alt="" className="h-10 w-8 rounded object-cover" />}
|
||||
<span>{library.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-1.5 text-ink-100">
|
||||
<LibraryRootsCell library={library} {...actions} />
|
||||
</td>
|
||||
@@ -170,12 +177,18 @@ function RootActionButtons({ library, root, draft, ...actions }: RootEditorProps
|
||||
)
|
||||
}
|
||||
|
||||
function LibraryActionsCell({ library, onScanLibrary, onRemoveLibrary }: LibraryTableRowProps) {
|
||||
function LibraryActionsCell({ library, onScanLibrary, onRemoveLibrary, onAddLibraryRoot, onEditLibraryCover }: LibraryTableRowProps) {
|
||||
return (
|
||||
<ActionMenu label="媒体库操作">
|
||||
<MenuButton icon={<RefreshCw size={14} />} label="扫描" onClick={() => onScanLibrary(library)}>
|
||||
扫描
|
||||
</MenuButton>
|
||||
<MenuButton icon={<Plus size={14} />} label="添加来源" onClick={() => onAddLibraryRoot(library)}>
|
||||
添加来源
|
||||
</MenuButton>
|
||||
<MenuButton icon={<Image size={14} />} label="自定义封面" onClick={() => onEditLibraryCover(library)}>
|
||||
自定义封面
|
||||
</MenuButton>
|
||||
<MenuButton danger icon={<Trash2 size={14} />} label="删除" onClick={() => onRemoveLibrary(library)}>
|
||||
删除
|
||||
</MenuButton>
|
||||
|
||||
@@ -129,7 +129,9 @@ export function LibrariesContent({ previews }: { previews: LibraryPreview[] }) {
|
||||
|
||||
function LibraryEntryCard({ preview }: { preview: LibraryPreview }) {
|
||||
const library = preview.library
|
||||
const artwork = libraryArtworkItems(preview.cards)
|
||||
const artwork = library.cover_url
|
||||
? [{ src: library.cover_url, version: library.updated_at }]
|
||||
: libraryArtworkItems(preview.cards)
|
||||
const displayPath = libraryDisplayPath(library.path)
|
||||
|
||||
return (
|
||||
@@ -137,7 +139,7 @@ function LibraryEntryCard({ preview }: { preview: LibraryPreview }) {
|
||||
to={`/library/${library.id}`}
|
||||
className="group flex overflow-hidden rounded-3xl border border-sand-200 bg-white p-3 shadow-card transition-all hover:-translate-y-0.5 hover:border-brand-200 hover:shadow-card-hover"
|
||||
>
|
||||
<div className="grid h-24 w-36 shrink-0 grid-cols-2 gap-1 overflow-hidden rounded-2xl bg-[linear-gradient(135deg,#fff7ed,#f8fafc)]">
|
||||
<div className={`grid h-24 w-36 shrink-0 gap-1 overflow-hidden rounded-2xl bg-[linear-gradient(135deg,#fff7ed,#f8fafc)] ${artwork.length > 1 ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||
{artwork.length > 0 ? (
|
||||
artwork.map(({ src, version }, index) => (
|
||||
<img
|
||||
|
||||
@@ -5,6 +5,13 @@ export const generalSettingsGroup: SettingGroup = {
|
||||
label: '常规',
|
||||
description: '语言 / 转码引擎参数(API 密钥请在管理后台 → 外部API 配置)',
|
||||
items: [
|
||||
{
|
||||
key: 'ui.hide_community_links_for_users',
|
||||
label: '对普通用户隐藏社区页脚链接',
|
||||
type: 'toggle',
|
||||
hint: '开启后,登录页及普通用户登录后的页脚不显示 TG 群组、开源仓库和作者主页;管理员仍可见。',
|
||||
defaultValue: 'false',
|
||||
},
|
||||
{
|
||||
key: 'tmdb.language',
|
||||
label: 'TMDb 元数据语言',
|
||||
|
||||
@@ -31,6 +31,7 @@ function useCreateLibraryForm(refresh: () => Promise<void>) {
|
||||
const [name, setName] = useState('')
|
||||
const [roots, setRoots] = useState<RootDraft[]>([emptyRootDraft()])
|
||||
const [type, setType] = useState('movie')
|
||||
const [coverURL, setCoverURL] = useState('')
|
||||
|
||||
const handleCreate = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -40,10 +41,11 @@ function useCreateLibraryForm(refresh: () => Promise<void>) {
|
||||
toast.error('请至少填写一个路径')
|
||||
return
|
||||
}
|
||||
await libraryAPI.createWithRoots(name, type, payload)
|
||||
await libraryAPI.createWithRoots(name, type, payload, coverURL.trim())
|
||||
toast.success('媒体库已保存')
|
||||
setName('')
|
||||
setRoots([emptyRootDraft()])
|
||||
setCoverURL('')
|
||||
await refresh()
|
||||
} catch (err: unknown) {
|
||||
toast.error(apiErrorMessage(err, '创建失败'))
|
||||
@@ -57,9 +59,11 @@ function useCreateLibraryForm(refresh: () => Promise<void>) {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
coverURL,
|
||||
roots,
|
||||
setName,
|
||||
setType,
|
||||
setCoverURL,
|
||||
updateRoot,
|
||||
addRoot: () => setRoots((prev) => [...prev, emptyRootDraft()]),
|
||||
removeRoot: (index: number) => setRoots((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index))),
|
||||
@@ -153,5 +157,22 @@ function useLibraryActions(refresh: () => Promise<void>) {
|
||||
await refresh()
|
||||
}
|
||||
|
||||
return { scanLibrary, removeLibrary }
|
||||
const addLibraryRoot = async (library: Library) => {
|
||||
const path = window.prompt(`为「${library.name}」添加来源目录:`)
|
||||
if (!path?.trim()) return
|
||||
const name = window.prompt('来源名称(可选):') ?? ''
|
||||
await libraryAPI.addRoot(library.id, { path: path.trim(), name: name.trim(), enabled: true })
|
||||
toast.success('来源目录已添加')
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const editLibraryCover = async (library: Library) => {
|
||||
const coverURL = window.prompt('自定义封面 URL(留空可清除):', library.cover_url ?? '')
|
||||
if (coverURL === null) return
|
||||
await libraryAPI.update(library.id, { cover_url: coverURL.trim() })
|
||||
toast.success(coverURL.trim() ? '媒体库封面已保存' : '媒体库封面已清除')
|
||||
await refresh()
|
||||
}
|
||||
|
||||
return { scanLibrary, removeLibrary, addLibraryRoot, editLibraryCover }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Library {
|
||||
name: string
|
||||
path: string
|
||||
type: string
|
||||
cover_url?: string
|
||||
enabled: boolean
|
||||
roots?: LibraryRoot[]
|
||||
created_at: string
|
||||
|
||||
Reference in New Issue
Block a user