mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-18 15:04:41 +08:00
feat: implement iptv
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Cat, Clover, Film, Home, Star, Tv } from 'lucide-react';
|
||||
import { Cat, Clover, Film, Home, Radio, Star, Tv } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -42,6 +42,11 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
label: '综艺',
|
||||
href: '/douban?type=show',
|
||||
},
|
||||
{
|
||||
icon: Radio,
|
||||
label: '直播',
|
||||
href: '/live',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||
return (
|
||||
<div className='w-full min-h-screen'>
|
||||
{/* 移动端头部 */}
|
||||
<MobileHeader showBackButton={['/play'].includes(activePath)} />
|
||||
<MobileHeader showBackButton={['/play', '/live'].includes(activePath)} />
|
||||
|
||||
{/* 主要布局容器 */}
|
||||
<div className='flex md:grid md:grid-cols-[auto_1fr] w-full min-h-screen md:min-h-auto'>
|
||||
@@ -26,7 +26,7 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||
{/* 主内容区域 */}
|
||||
<div className='relative min-w-0 flex-1 transition-all duration-300'>
|
||||
{/* 桌面端左上角返回按钮 */}
|
||||
{['/play'].includes(activePath) && (
|
||||
{['/play', '/live'].includes(activePath) && (
|
||||
<div className='absolute top-3 left-1 z-20 hidden md:flex'>
|
||||
<BackButton />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Cat, Clover, Film, Home, Menu, Search, Star, Tv } from 'lucide-react';
|
||||
import { Cat, Clover, Film, Home, Menu, Radio, Search, Star, Tv } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
@@ -145,6 +145,11 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
label: '综艺',
|
||||
href: '/douban?type=show',
|
||||
},
|
||||
{
|
||||
icon: Radio,
|
||||
label: '直播',
|
||||
href: '/live',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -66,6 +66,7 @@ export const UserMenu: React.FC = () => {
|
||||
const [doubanProxyUrl, setDoubanProxyUrl] = useState('');
|
||||
const [enableOptimization, setEnableOptimization] = useState(true);
|
||||
const [fluidSearch, setFluidSearch] = useState(true);
|
||||
const [liveDirectConnect, setLiveDirectConnect] = useState(false);
|
||||
const [doubanDataSource, setDoubanDataSource] = useState('melody-cdn-sharon');
|
||||
const [doubanImageProxyType, setDoubanImageProxyType] = useState('melody-cdn-sharon');
|
||||
const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState('');
|
||||
@@ -191,6 +192,11 @@ export const UserMenu: React.FC = () => {
|
||||
} else if (defaultFluidSearch !== undefined) {
|
||||
setFluidSearch(defaultFluidSearch);
|
||||
}
|
||||
|
||||
const savedLiveDirectConnect = localStorage.getItem('liveDirectConnect');
|
||||
if (savedLiveDirectConnect !== null) {
|
||||
setLiveDirectConnect(JSON.parse(savedLiveDirectConnect));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -366,6 +372,13 @@ export const UserMenu: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLiveDirectConnectToggle = (value: boolean) => {
|
||||
setLiveDirectConnect(value);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('liveDirectConnect', JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubanDataSourceChange = (value: string) => {
|
||||
setDoubanDataSource(value);
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -426,6 +439,7 @@ export const UserMenu: React.FC = () => {
|
||||
setDefaultAggregateSearch(true);
|
||||
setEnableOptimization(true);
|
||||
setFluidSearch(defaultFluidSearch);
|
||||
setLiveDirectConnect(false);
|
||||
setDoubanProxyUrl(defaultDoubanProxy);
|
||||
setDoubanDataSource(defaultDoubanProxyType);
|
||||
setDoubanImageProxyType(defaultDoubanImageProxyType);
|
||||
@@ -435,6 +449,7 @@ export const UserMenu: React.FC = () => {
|
||||
localStorage.setItem('defaultAggregateSearch', JSON.stringify(true));
|
||||
localStorage.setItem('enableOptimization', JSON.stringify(true));
|
||||
localStorage.setItem('fluidSearch', JSON.stringify(defaultFluidSearch));
|
||||
localStorage.setItem('liveDirectConnect', JSON.stringify(false));
|
||||
localStorage.setItem('doubanProxyUrl', defaultDoubanProxy);
|
||||
localStorage.setItem('doubanDataSource', defaultDoubanProxyType);
|
||||
localStorage.setItem('doubanImageProxyType', defaultDoubanImageProxyType);
|
||||
@@ -922,6 +937,30 @@ export const UserMenu: React.FC = () => {
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 直播视频浏览器直连 */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
IPTV 视频浏览器直连
|
||||
</h4>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
开启 IPTV 视频浏览器直连时,需要自备 Allow CORS 插件
|
||||
</p>
|
||||
</div>
|
||||
<label className='flex items-center cursor-pointer'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='sr-only peer'
|
||||
checked={liveDirectConnect}
|
||||
onChange={(e) => handleLiveDirectConnectToggle(e.target.checked)}
|
||||
/>
|
||||
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
|
||||
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部说明 */}
|
||||
|
||||
@@ -672,7 +672,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
{/* 年份徽章 */}
|
||||
{config.showYear && actualYear && actualYear !== 'unknown' && actualYear.trim() !== '' && (
|
||||
<div
|
||||
className={`absolute top-2 bg-black/50 text-white text-xs font-medium px-2 py-1 rounded backdrop-blur-sm shadow-sm transition-all duration-300 ease-out group-hover:opacity-90 left-2`}
|
||||
className="absolute top-2 bg-black/50 text-white text-xs font-medium px-2 py-1 rounded backdrop-blur-sm shadow-sm transition-all duration-300 ease-out group-hover:opacity-90 left-2"
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
|
||||
Reference in New Issue
Block a user