openlist增加路径元信息配置分类和自动刷新链接功能
This commit is contained in:
+211
-1
@@ -212,7 +212,7 @@ const AlertModal = ({
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 transition-opacity duration-200 ${
|
||||
className={`fixed inset-0 bg-black bg-opacity-50 z-[10050] flex items-center justify-center p-4 transition-opacity duration-200 ${
|
||||
isVisible ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
>
|
||||
@@ -3476,6 +3476,9 @@ const OpenListConfigComponent = ({
|
||||
'hybrid'
|
||||
);
|
||||
const [disableVideoPreview, setDisableVideoPreview] = useState(false);
|
||||
const [pathMetaRows, setPathMetaRows] = useState<
|
||||
Array<{ path: string; category: string; refresh14m: boolean }>
|
||||
>([]);
|
||||
const [videos, setVideos] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [scanProgress, setScanProgress] = useState<{
|
||||
@@ -3485,6 +3488,7 @@ const OpenListConfigComponent = ({
|
||||
} | null>(null);
|
||||
const [correctDialogOpen, setCorrectDialogOpen] = useState(false);
|
||||
const [selectedVideo, setSelectedVideo] = useState<any | null>(null);
|
||||
const [pathMetaDialogOpen, setPathMetaDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.OpenListConfig) {
|
||||
@@ -3514,6 +3518,14 @@ const OpenListConfigComponent = ({
|
||||
setDisableVideoPreview(
|
||||
config.OpenListConfig.DisableVideoPreview || false
|
||||
);
|
||||
const pathMeta = config.OpenListConfig.PathMeta || {};
|
||||
setPathMetaRows(
|
||||
Object.entries(pathMeta).map(([path, meta]) => ({
|
||||
path,
|
||||
category: meta?.category || '',
|
||||
refresh14m: Boolean(meta?.refresh14m),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
@@ -3548,6 +3560,22 @@ const OpenListConfigComponent = ({
|
||||
const handleSave = async () => {
|
||||
await withLoading('saveOpenList', async () => {
|
||||
try {
|
||||
// 路径元信息:序列化为 map(匹配时按最长前缀)
|
||||
if (pathMetaRows.some((row) => !(row.path || '').trim())) {
|
||||
throw new Error('路径元信息中的路径不能为空');
|
||||
}
|
||||
const pathMetaPayload: Record<
|
||||
string,
|
||||
{ category: string; refresh14m: boolean }
|
||||
> = {};
|
||||
for (const row of pathMetaRows) {
|
||||
const p = (row.path || '').trim();
|
||||
pathMetaPayload[p] = {
|
||||
category: (row.category || '').trim(),
|
||||
refresh14m: Boolean(row.refresh14m),
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch('/api/admin/openlist', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -3566,6 +3594,7 @@ const OpenListConfigComponent = ({
|
||||
ScanInterval: scanInterval,
|
||||
ScanMode: scanMode,
|
||||
DisableVideoPreview: disableVideoPreview,
|
||||
PathMeta: pathMetaPayload,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4070,6 +4099,187 @@ const OpenListConfigComponent = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
|
||||
<div>
|
||||
<h3 className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
路径元信息
|
||||
</h3>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
为指定路径下的影片设置分类,以及播放时是否自动刷新链接(约 14 分钟)
|
||||
{pathMetaRows.length > 0
|
||||
? ` · 已配置 ${pathMetaRows.length} 条`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setPathMetaDialogOpen(true)}
|
||||
disabled={!enabled}
|
||||
className={`${buttonStyles.primary} text-sm ${
|
||||
!enabled ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{pathMetaDialogOpen &&
|
||||
createPortal(
|
||||
<div
|
||||
className='fixed inset-0 bg-black bg-opacity-50 z-[10002] flex items-center justify-center p-4'
|
||||
onClick={() => setPathMetaDialogOpen(false)}
|
||||
onTouchMove={(e) => e.preventDefault()}
|
||||
onWheel={(e) => e.preventDefault()}
|
||||
style={{ touchAction: 'none' }}
|
||||
>
|
||||
<div
|
||||
className='w-full max-w-3xl max-h-[85vh] flex flex-col rounded-xl bg-white dark:bg-gray-900 shadow-xl border border-gray-200 dark:border-gray-700'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onTouchMove={(e) => e.stopPropagation()}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
style={{ touchAction: 'auto' }}
|
||||
>
|
||||
<div className='flex items-center justify-between px-5 py-4 border-b border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-base font-medium text-gray-900 dark:text-white'>
|
||||
路径元信息
|
||||
</h3>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setPathMetaDialogOpen(false)}
|
||||
className='text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-sm px-2 py-1'
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='px-5 py-3 text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800'>
|
||||
填写目录路径即可作用于其下所有影片(如 /videos)。更具体的路径优先。改完后点「保存配置」才会生效。
|
||||
</div>
|
||||
|
||||
<div className='flex-1 overflow-y-auto px-5 py-4 space-y-2'>
|
||||
{pathMetaRows.length === 0 ? (
|
||||
<p className='text-sm text-gray-400 dark:text-gray-500 text-center py-8'>
|
||||
暂无配置,点击下方「添加」开始
|
||||
</p>
|
||||
) : (
|
||||
pathMetaRows.map((row, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='grid grid-cols-1 md:grid-cols-12 gap-2 items-center'
|
||||
>
|
||||
<div className='md:col-span-5'>
|
||||
<input
|
||||
type='text'
|
||||
value={row.path}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setPathMetaRows((rows) =>
|
||||
rows.map((r, i) =>
|
||||
i === index ? { ...r, path: value } : r
|
||||
)
|
||||
);
|
||||
}}
|
||||
placeholder='路径,如 /videos'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent'
|
||||
/>
|
||||
</div>
|
||||
<div className='md:col-span-3'>
|
||||
<input
|
||||
type='text'
|
||||
value={row.category}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setPathMetaRows((rows) =>
|
||||
rows.map((r, i) =>
|
||||
i === index ? { ...r, category: value } : r
|
||||
)
|
||||
);
|
||||
}}
|
||||
placeholder='分类,如 动漫'
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent'
|
||||
/>
|
||||
</div>
|
||||
<div className='md:col-span-3 flex items-center gap-2'>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap'>
|
||||
播放自动刷新
|
||||
</span>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() =>
|
||||
setPathMetaRows((rows) =>
|
||||
rows.map((r, i) =>
|
||||
i === index
|
||||
? { ...r, refresh14m: !r.refresh14m }
|
||||
: r
|
||||
)
|
||||
)
|
||||
}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${
|
||||
row.refresh14m
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
aria-label='播放自动刷新'
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
row.refresh14m
|
||||
? 'translate-x-6'
|
||||
: 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='md:col-span-1 flex justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() =>
|
||||
setPathMetaRows((rows) =>
|
||||
rows.filter((_, i) => i !== index)
|
||||
)
|
||||
}
|
||||
className='px-2 py-1 text-sm text-red-600 hover:text-red-700 dark:text-red-400'
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between gap-3 px-5 py-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() =>
|
||||
setPathMetaRows((rows) => [
|
||||
...rows,
|
||||
{ path: '', category: '', refresh14m: false },
|
||||
])
|
||||
}
|
||||
className={buttonStyles.primary}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
if (pathMetaRows.some((row) => !(row.path || '').trim())) {
|
||||
showError('路径不能为空', showAlert);
|
||||
return;
|
||||
}
|
||||
setPathMetaDialogOpen(false);
|
||||
}}
|
||||
className={buttonStyles.success}
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
<div className='flex gap-3'>
|
||||
<button
|
||||
onClick={handleCheckConnectivity}
|
||||
|
||||
Reference in New Issue
Block a user