mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-03-13 01:17:29 +08:00
feat: add local settings
This commit is contained in:
@@ -50,7 +50,6 @@ interface SiteConfig {
|
||||
Announcement: string;
|
||||
SearchDownstreamMaxPage: number;
|
||||
SiteInterfaceCacheTime: number;
|
||||
SearchResultDefaultAggregate: boolean;
|
||||
}
|
||||
|
||||
// 视频源数据类型
|
||||
@@ -948,7 +947,6 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
||||
Announcement: '',
|
||||
SearchDownstreamMaxPage: 1,
|
||||
SiteInterfaceCacheTime: 7200,
|
||||
SearchResultDefaultAggregate: false,
|
||||
});
|
||||
// 保存状态
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -1094,45 +1092,6 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 默认按标题和年份聚合 */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<label
|
||||
className={`text-gray-700 dark:text-gray-300 ${
|
||||
isD1Storage ? 'opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
搜索结果默认按标题和年份聚合
|
||||
{isD1Storage && (
|
||||
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
|
||||
(D1 环境下不可修改)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<button
|
||||
onClick={() =>
|
||||
!isD1Storage &&
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
SearchResultDefaultAggregate: !prev.SearchResultDefaultAggregate,
|
||||
}))
|
||||
}
|
||||
disabled={isD1Storage}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
||||
siteSettings.SearchResultDefaultAggregate
|
||||
? 'bg-green-600'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
} ${isD1Storage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
siteSettings.SearchResultDefaultAggregate
|
||||
? 'translate-x-6'
|
||||
: 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className='flex justify-end'>
|
||||
<button
|
||||
|
||||
@@ -33,13 +33,11 @@ export async function POST(request: NextRequest) {
|
||||
Announcement,
|
||||
SearchDownstreamMaxPage,
|
||||
SiteInterfaceCacheTime,
|
||||
SearchResultDefaultAggregate,
|
||||
} = body as {
|
||||
SiteName: string;
|
||||
Announcement: string;
|
||||
SearchDownstreamMaxPage: number;
|
||||
SiteInterfaceCacheTime: number;
|
||||
SearchResultDefaultAggregate: boolean;
|
||||
};
|
||||
|
||||
// 参数校验
|
||||
@@ -47,8 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
typeof SiteName !== 'string' ||
|
||||
typeof Announcement !== 'string' ||
|
||||
typeof SearchDownstreamMaxPage !== 'number' ||
|
||||
typeof SiteInterfaceCacheTime !== 'number' ||
|
||||
typeof SearchResultDefaultAggregate !== 'boolean'
|
||||
typeof SiteInterfaceCacheTime !== 'number'
|
||||
) {
|
||||
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
||||
}
|
||||
@@ -73,7 +70,6 @@ export async function POST(request: NextRequest) {
|
||||
Announcement,
|
||||
SearchDownstreamMaxPage,
|
||||
SiteInterfaceCacheTime,
|
||||
SearchResultDefaultAggregate,
|
||||
};
|
||||
|
||||
// 写入数据库
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
// 设置缓存头(可选)
|
||||
headers.set('Cache-Control', 'public, max-age=86400'); // 缓存24小时
|
||||
headers.set('Cache-Control', 'public, max-age=15720000'); // 缓存半年
|
||||
|
||||
// 直接返回图片流
|
||||
return new Response(imageResponse.body, {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DoubanItem, DoubanResult } from '@/lib/types';
|
||||
import { getDoubanData } from '@/lib/douban.client';
|
||||
import { DoubanItem } from '@/lib/types';
|
||||
|
||||
import DoubanCardSkeleton from '@/components/DoubanCardSkeleton';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
@@ -45,15 +46,12 @@ function DoubanPageClient() {
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(
|
||||
`/api/douban?type=${type}&tag=${tag}&pageSize=25&pageStart=0`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取豆瓣数据失败');
|
||||
}
|
||||
|
||||
const data: DoubanResult = await response.json();
|
||||
const data = await getDoubanData({
|
||||
type: type as 'tv' | 'movie',
|
||||
tag,
|
||||
pageSize: 25,
|
||||
pageStart: 0,
|
||||
});
|
||||
|
||||
if (data.code === 200) {
|
||||
setDoubanData(data.list);
|
||||
@@ -78,17 +76,12 @@ function DoubanPageClient() {
|
||||
try {
|
||||
setIsLoadingMore(true);
|
||||
|
||||
const response = await fetch(
|
||||
`/api/douban?type=${type}&tag=${tag}&pageSize=25&pageStart=${
|
||||
currentPage * 25
|
||||
}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取豆瓣数据失败');
|
||||
}
|
||||
|
||||
const data: DoubanResult = await response.json();
|
||||
const data = await getDoubanData({
|
||||
type: type as 'tv' | 'movie',
|
||||
tag,
|
||||
pageSize: 25,
|
||||
pageStart: currentPage * 25,
|
||||
});
|
||||
|
||||
if (data.code === 200) {
|
||||
setDoubanData((prev) => [...prev, ...data.list]);
|
||||
|
||||
@@ -40,21 +40,17 @@ export default async function RootLayout({
|
||||
process.env.ANNOUNCEMENT ||
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
|
||||
let enableRegister = process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true';
|
||||
let aggregateSearchResult =
|
||||
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false';
|
||||
if (process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'd1') {
|
||||
const config = await getConfig();
|
||||
siteName = config.SiteConfig.SiteName;
|
||||
announcement = config.SiteConfig.Announcement;
|
||||
enableRegister = config.UserConfig.AllowRegister;
|
||||
aggregateSearchResult = config.SiteConfig.SearchResultDefaultAggregate;
|
||||
}
|
||||
|
||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||
const runtimeConfig = {
|
||||
STORAGE_TYPE: process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage',
|
||||
ENABLE_REGISTER: enableRegister,
|
||||
AGGREGATE_SEARCH_RESULT: aggregateSearchResult,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console */
|
||||
|
||||
'use client';
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
getAllPlayRecords,
|
||||
subscribeToDataUpdates,
|
||||
} from '@/lib/db.client';
|
||||
import { DoubanItem, DoubanResult } from '@/lib/types';
|
||||
import { getDoubanData } from '@/lib/douban.client';
|
||||
import { DoubanItem } from '@/lib/types';
|
||||
|
||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||
import ContinueWatching from '@/components/ContinueWatching';
|
||||
@@ -63,20 +64,20 @@ function HomeClient() {
|
||||
setLoading(true);
|
||||
|
||||
// 并行获取热门电影和热门剧集
|
||||
const [moviesResponse, tvShowsResponse] = await Promise.all([
|
||||
fetch('/api/douban?type=movie&tag=热门'),
|
||||
fetch('/api/douban?type=tv&tag=热门'),
|
||||
const [moviesData, tvShowsData] = await Promise.all([
|
||||
getDoubanData({ type: 'movie', tag: '热门' }),
|
||||
getDoubanData({ type: 'tv', tag: '热门' }),
|
||||
]);
|
||||
|
||||
if (moviesResponse.ok) {
|
||||
const moviesData: DoubanResult = await moviesResponse.json();
|
||||
if (moviesData.code === 200) {
|
||||
setHotMovies(moviesData.list);
|
||||
}
|
||||
|
||||
if (tvShowsResponse.ok) {
|
||||
const tvShowsData: DoubanResult = await tvShowsResponse.json();
|
||||
if (tvShowsData.code === 200) {
|
||||
setHotTvShows(tvShowsData.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取豆瓣数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
toggleFavorite,
|
||||
} from '@/lib/db.client';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
|
||||
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||
|
||||
import EpisodeSelector from '@/components/EpisodeSelector';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
@@ -1611,7 +1611,7 @@ function PlayPageClient() {
|
||||
<div className='bg-gray-300 dark:bg-gray-700 aspect-[2/3] flex items-center justify-center rounded-xl overflow-hidden'>
|
||||
{videoCover ? (
|
||||
<img
|
||||
src={videoCover}
|
||||
src={processImageUrl(videoCover)}
|
||||
alt={videoTitle}
|
||||
className='w-full h-full object-cover'
|
||||
/>
|
||||
|
||||
@@ -28,14 +28,20 @@ function SearchPageClient() {
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
|
||||
// 视图模式:聚合(agg) 或 全部(all),默认值由环境变量 NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT 决定
|
||||
const defaultAggregate =
|
||||
typeof window !== 'undefined' &&
|
||||
Boolean((window as any).RUNTIME_CONFIG?.AGGREGATE_SEARCH_RESULT);
|
||||
// 获取默认聚合设置:只读取用户本地设置,默认为 true
|
||||
const getDefaultAggregate = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const userSetting = localStorage.getItem('defaultAggregateSearch');
|
||||
if (userSetting !== null) {
|
||||
return JSON.parse(userSetting);
|
||||
}
|
||||
}
|
||||
return true; // 默认启用聚合
|
||||
};
|
||||
|
||||
const [viewMode, setViewMode] = useState<'agg' | 'all'>(
|
||||
defaultAggregate ? 'agg' : 'all'
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<'agg' | 'all'>(() => {
|
||||
return getDefaultAggregate() ? 'agg' : 'all';
|
||||
});
|
||||
|
||||
// 聚合后的结果(按标题和年份分组)
|
||||
const aggregatedResults = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user