增加tvbox订阅功能,自带去广告

This commit is contained in:
mtvpls
2025-12-05 22:33:57 +08:00
parent cc99f4c945
commit e028ba60d7
6 changed files with 640 additions and 5 deletions
+179 -2
View File
@@ -5,9 +5,11 @@
import {
Check,
ChevronDown,
Copy,
ExternalLink,
KeyRound,
LogOut,
Rss,
Settings,
Shield,
User,
@@ -36,14 +38,21 @@ export const UserMenu: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
const [isSubscribeOpen, setIsSubscribeOpen] = useState(false);
const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false);
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
const [storageType, setStorageType] = useState<string>('localstorage');
const [mounted, setMounted] = useState(false);
// 订阅相关状态
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
const [subscribeUrl, setSubscribeUrl] = useState('');
const [copySuccess, setCopySuccess] = useState(false);
const [adFilterEnabled, setAdFilterEnabled] = useState(true); // 去广告开关,默认开启
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
useEffect(() => {
if (isSettingsOpen || isChangePasswordOpen) {
if (isSettingsOpen || isChangePasswordOpen || isSubscribeOpen) {
const body = document.body;
const html = document.documentElement;
@@ -62,7 +71,7 @@ export const UserMenu: React.FC = () => {
html.style.overflow = originalHtmlOverflow;
};
}
}, [isSettingsOpen, isChangePasswordOpen]);
}, [isSettingsOpen, isChangePasswordOpen, isSubscribeOpen]);
// 设置相关状态
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
@@ -117,6 +126,28 @@ export const UserMenu: React.FC = () => {
setMounted(true);
}, []);
// 获取订阅配置
useEffect(() => {
const fetchSubscribeConfig = async () => {
try {
// 获取当前浏览器地址
const currentOrigin = window.location.origin;
const response = await fetch(`/api/tvbox/config?origin=${encodeURIComponent(currentOrigin)}&adFilter=${adFilterEnabled}`);
if (response.ok) {
const data = await response.json();
setSubscribeEnabled(data.enabled);
setSubscribeUrl(data.url);
}
} catch (error) {
console.error('获取订阅配置失败:', error);
}
};
if (typeof window !== 'undefined') {
fetchSubscribeConfig();
}
}, [adFilterEnabled]); // 依赖 adFilterEnabled,当开关改变时重新获取
// 获取认证信息和存储类型
useEffect(() => {
if (typeof window !== 'undefined') {
@@ -275,6 +306,29 @@ export const UserMenu: React.FC = () => {
setPasswordError('');
};
const handleSubscribe = () => {
setIsOpen(false);
setIsSubscribeOpen(true);
setCopySuccess(false);
};
const handleCloseSubscribe = () => {
setIsSubscribeOpen(false);
setCopySuccess(false);
};
const handleCopySubscribeUrl = async () => {
try {
await navigator.clipboard.writeText(subscribeUrl);
setCopySuccess(true);
setTimeout(() => {
setCopySuccess(false);
}, 2000);
} catch (error) {
console.error('复制失败:', error);
}
};
const handleSubmitChangePassword = async () => {
setPasswordError('');
@@ -560,6 +614,17 @@ export const UserMenu: React.FC = () => {
</button>
)}
{/* 订阅按钮 */}
{subscribeEnabled && (
<button
onClick={handleSubscribe}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<Rss className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
</button>
)}
{/* 分割线 */}
<div className='my-1 border-t border-gray-200 dark:border-gray-700'></div>
@@ -1026,6 +1091,113 @@ export const UserMenu: React.FC = () => {
</>
);
// 订阅面板内容
const subscribePanel = (
<>
{/* 背景遮罩 */}
<div
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
onClick={handleCloseSubscribe}
onTouchMove={(e) => {
e.preventDefault();
}}
onWheel={(e) => {
e.preventDefault();
}}
style={{
touchAction: 'none',
}}
/>
{/* 订阅面板 */}
<div
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
<div
className='h-full p-6'
data-panel-content
onTouchMove={(e) => {
e.stopPropagation();
}}
style={{
touchAction: 'auto',
}}
>
{/* 标题栏 */}
<div className='flex items-center justify-between mb-6'>
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h3>
<button
onClick={handleCloseSubscribe}
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
aria-label='Close'
>
<X className='w-full h-full' />
</button>
</div>
{/* 内容 */}
<div className='space-y-4'>
{/* 去广告开关 */}
<div className='flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
广
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
广
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={adFilterEnabled}
onChange={(e) => setAdFilterEnabled(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>
{/* TVBOX订阅 */}
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
TVBOX订阅
</h4>
<div className='flex gap-2'>
<input
type='text'
className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 cursor-not-allowed'
value={subscribeUrl}
disabled
readOnly
/>
<button
onClick={handleCopySubscribeUrl}
className='px-4 py-2 bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600 text-white text-sm font-medium rounded-md transition-colors flex items-center gap-2'
>
<Copy className='w-4 h-4' />
{copySuccess ? '已复制' : '复制'}
</button>
</div>
</div>
</div>
{/* 底部说明 */}
<div className='mt-6 pt-4 border-t border-gray-200 dark:border-gray-700'>
<p className='text-xs text-gray-500 dark:text-gray-400 text-center'>
TVBOX应用中使用
</p>
</div>
</div>
</div>
</>
);
// 修改密码面板内容
const changePasswordPanel = (
<>
@@ -1171,6 +1343,11 @@ export const UserMenu: React.FC = () => {
mounted &&
createPortal(changePasswordPanel, document.body)}
{/* 使用 Portal 将订阅面板渲染到 document.body */}
{isSubscribeOpen &&
mounted &&
createPortal(subscribePanel, document.body)}
{/* 版本面板 */}
<VersionPanel
isOpen={isVersionPanelOpen}