通知增加区域switch,增加免责声明

This commit is contained in:
mtvpls
2026-07-01 10:21:51 +08:00
parent 94f4c38575
commit ba34019bde
2 changed files with 410 additions and 129 deletions
+222 -3
View File
@@ -12775,6 +12775,8 @@ const SuwayomiConfigComponent = ({
const [defaultLang, setDefaultLang] = useState('zh'); const [defaultLang, setDefaultLang] = useState('zh');
const [sourceIds, setSourceIds] = useState(''); const [sourceIds, setSourceIds] = useState('');
const [maxSources, setMaxSources] = useState(10); const [maxSources, setMaxSources] = useState(10);
const [showMangaDisclaimer, setShowMangaDisclaimer] = useState(false);
const [mangaCountdown, setMangaCountdown] = useState(10);
useEffect(() => { useEffect(() => {
if (config?.SuwayomiConfig) { if (config?.SuwayomiConfig) {
@@ -12789,6 +12791,14 @@ const SuwayomiConfigComponent = ({
} }
}, [config]); }, [config]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (showMangaDisclaimer && mangaCountdown > 0) {
timer = setTimeout(() => setMangaCountdown(mangaCountdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [showMangaDisclaimer, mangaCountdown]);
const buildConfig = () => ({ const buildConfig = () => ({
Enabled: enabled, Enabled: enabled,
ServerURL: serverURL, ServerURL: serverURL,
@@ -12896,7 +12906,14 @@ const SuwayomiConfigComponent = ({
</p> </p>
</div> </div>
<button <button
onClick={() => setEnabled(!enabled)} onClick={() => {
if (!enabled) {
setShowMangaDisclaimer(true);
setMangaCountdown(10);
} else {
setEnabled(false);
}
}}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700' enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
}`} }`}
@@ -12909,6 +12926,62 @@ const SuwayomiConfigComponent = ({
</button> </button>
</div> </div>
{/* 漫画展馆免责声明弹窗 */}
{showMangaDisclaimer &&
createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-red-200 dark:border-red-800'>
<div className='p-6'>
<div className='flex justify-center mb-4'>
<AlertTriangle className='w-12 h-12 text-red-500' />
</div>
<h3 className='text-xl font-bold text-gray-900 dark:text-gray-100 mb-4 text-center'>
</h3>
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6'>
<p className='text-sm text-gray-700 dark:text-gray-300 leading-relaxed'>
使
使使
</p>
</div>
<div className='flex gap-3 justify-center'>
<button
onClick={() => {
setShowMangaDisclaimer(false);
setMangaCountdown(10);
}}
className={buttonStyles.secondary}
>
</button>
<button
onClick={() => {
setEnabled(true);
setShowMangaDisclaimer(false);
setMangaCountdown(10);
}}
disabled={mangaCountdown > 0}
className={
mangaCountdown > 0
? buttonStyles.disabled
: buttonStyles.danger
}
>
{mangaCountdown > 0
? `确认 (${mangaCountdown}s)`
: '确认启用'}
</button>
</div>
</div>
</div>
</div>,
document.body
)}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Suwayomi Suwayomi
@@ -13077,6 +13150,8 @@ const OPDSConfigComponent = ({
const [legadoSubscriptions, setLegadoSubscriptions] = useState< const [legadoSubscriptions, setLegadoSubscriptions] = useState<
NonNullable<AdminConfig['OPDSConfig']>['LegadoSubscriptions'] NonNullable<AdminConfig['OPDSConfig']>['LegadoSubscriptions']
>([]); >([]);
const [showBooksDisclaimer, setShowBooksDisclaimer] = useState(false);
const [booksCountdown, setBooksCountdown] = useState(10);
useEffect(() => { useEffect(() => {
if (!config?.OPDSConfig) return; if (!config?.OPDSConfig) return;
@@ -13103,6 +13178,14 @@ const OPDSConfigComponent = ({
setEditingIndex(null); setEditingIndex(null);
}, [config]); }, [config]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (showBooksDisclaimer && booksCountdown > 0) {
timer = setTimeout(() => setBooksCountdown(booksCountdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [showBooksDisclaimer, booksCountdown]);
const updateSource = (index: number, patch: Partial<BookSource>) => { const updateSource = (index: number, patch: Partial<BookSource>) => {
setSources((prev) => setSources((prev) =>
prev.map((item, idx) => (idx === index ? { ...item, ...patch } : item)) prev.map((item, idx) => (idx === index ? { ...item, ...patch } : item))
@@ -13326,7 +13409,14 @@ const OPDSConfigComponent = ({
</p> </p>
</div> </div>
<button <button
onClick={() => setEnabled(!enabled)} onClick={() => {
if (!enabled) {
setShowBooksDisclaimer(true);
setBooksCountdown(10);
} else {
setEnabled(false);
}
}}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enabled ? 'bg-amber-600' : 'bg-gray-200 dark:bg-gray-700' enabled ? 'bg-amber-600' : 'bg-gray-200 dark:bg-gray-700'
}`} }`}
@@ -13339,6 +13429,62 @@ const OPDSConfigComponent = ({
</button> </button>
</div> </div>
{/* 电子书馆免责声明弹窗 */}
{showBooksDisclaimer &&
createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-red-200 dark:border-red-800'>
<div className='p-6'>
<div className='flex justify-center mb-4'>
<AlertTriangle className='w-12 h-12 text-red-500' />
</div>
<h3 className='text-xl font-bold text-gray-900 dark:text-gray-100 mb-4 text-center'>
</h3>
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6'>
<p className='text-sm text-gray-700 dark:text-gray-300 leading-relaxed'>
使
使使
</p>
</div>
<div className='flex gap-3 justify-center'>
<button
onClick={() => {
setShowBooksDisclaimer(false);
setBooksCountdown(10);
}}
className={buttonStyles.secondary}
>
</button>
<button
onClick={() => {
setEnabled(true);
setShowBooksDisclaimer(false);
setBooksCountdown(10);
}}
disabled={booksCountdown > 0}
className={
booksCountdown > 0
? buttonStyles.disabled
: buttonStyles.danger
}
>
{booksCountdown > 0
? `确认 (${booksCountdown}s)`
: '确认启用'}
</button>
</div>
</div>
</div>
</div>,
document.body
)}
<div> <div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'> <label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
Feed Feed
@@ -15503,6 +15649,8 @@ const MusicConfigComponent = ({
const [baseUrl, setBaseUrl] = useState(''); const [baseUrl, setBaseUrl] = useState('');
const [token, setToken] = useState(''); const [token, setToken] = useState('');
const [proxyEnabled, setProxyEnabled] = useState(true); const [proxyEnabled, setProxyEnabled] = useState(true);
const [showMusicDisclaimer, setShowMusicDisclaimer] = useState(false);
const [musicCountdown, setMusicCountdown] = useState(10);
useEffect(() => { useEffect(() => {
if (config?.MusicConfig) { if (config?.MusicConfig) {
@@ -15513,6 +15661,14 @@ const MusicConfigComponent = ({
} }
}, [config]); }, [config]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (showMusicDisclaimer && musicCountdown > 0) {
timer = setTimeout(() => setMusicCountdown(musicCountdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [showMusicDisclaimer, musicCountdown]);
const handleSave = async () => { const handleSave = async () => {
await withLoading('saveMusicConfig', async () => { await withLoading('saveMusicConfig', async () => {
try { try {
@@ -15606,13 +15762,76 @@ const MusicConfigComponent = ({
<input <input
type='checkbox' type='checkbox'
checked={enabled} checked={enabled}
onChange={(e) => setEnabled(e.target.checked)} onChange={(e) => {
if (e.target.checked) {
setShowMusicDisclaimer(true);
setMusicCountdown(10);
} else {
setEnabled(false);
}
}}
className='sr-only peer' className='sr-only peer'
/> />
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[4px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div> <div className="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[4px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div>
</label> </label>
</div> </div>
{/* 音乐免责声明弹窗 */}
{showMusicDisclaimer &&
createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-red-200 dark:border-red-800'>
<div className='p-6'>
<div className='flex justify-center mb-4'>
<AlertTriangle className='w-12 h-12 text-red-500' />
</div>
<h3 className='text-xl font-bold text-gray-900 dark:text-gray-100 mb-4 text-center'>
</h3>
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6'>
<p className='text-sm text-gray-700 dark:text-gray-300 leading-relaxed'>
使
使使
</p>
</div>
<div className='flex gap-3 justify-center'>
<button
onClick={() => {
setShowMusicDisclaimer(false);
setMusicCountdown(10);
}}
className={buttonStyles.secondary}
>
</button>
<button
onClick={() => {
setEnabled(true);
setShowMusicDisclaimer(false);
setMusicCountdown(10);
}}
disabled={musicCountdown > 0}
className={
musicCountdown > 0
? buttonStyles.disabled
: buttonStyles.danger
}
>
{musicCountdown > 0
? `确认 (${musicCountdown}s)`
: '确认启用'}
</button>
</div>
</div>
</div>
</div>,
document.body
)}
<div className='space-y-4'> <div className='space-y-4'>
<div className='flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'> <div className='flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'>
<div> <div>
+65 -3
View File
@@ -2,6 +2,7 @@
import { Bell, Info, Mail, MonitorSmartphone, Send, X } from 'lucide-react'; import { Bell, Info, Mail, MonitorSmartphone, Send, X } from 'lucide-react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useState } from 'react';
interface EmailSettingsPanelProps { interface EmailSettingsPanelProps {
isOpen: boolean; isOpen: boolean;
@@ -30,6 +31,8 @@ interface EmailSettingsPanelProps {
statusType?: 'success' | 'error' | null; statusType?: 'success' | 'error' | null;
} }
type NotificationTab = 'email' | 'push' | 'telegram';
function Toggle({ function Toggle({
checked, checked,
disabled, disabled,
@@ -96,11 +99,21 @@ export function EmailSettingsPanel({
}: EmailSettingsPanelProps) { }: EmailSettingsPanelProps) {
if (!isOpen || !mounted) return null; if (!isOpen || !mounted) return null;
const [notificationTab, setNotificationTab] = useState<NotificationTab>('email');
const pushDisabled = const pushDisabled =
emailSettingsSaving || emailSettingsSaving ||
pushNotificationsBusy || pushNotificationsBusy ||
(!pushNotifications && (!pushNotificationsConfigured || !pushNotificationsSupported)); (!pushNotifications && (!pushNotificationsConfigured || !pushNotificationsSupported));
const telegramNotifyEnabled = Boolean(telegramEnabled);
const tabs = [
{ key: 'email' as const, label: '邮件通知', icon: Mail },
{ key: 'push' as const, label: '浏览器通知', icon: MonitorSmartphone },
{ key: 'telegram' as const, label: 'Telegram', icon: Send },
];
return createPortal( return createPortal(
<> <>
<div <div
@@ -139,6 +152,30 @@ export function EmailSettingsPanel({
</button> </button>
</div> </div>
{/* 三区域切换式导航 — 与电视访问一致 */}
<div className='mb-5 grid grid-cols-3 rounded-2xl bg-gray-100 p-1 dark:bg-white/10'>
{tabs.map((item) => {
const Icon = item.icon;
const active = notificationTab === item.key;
return (
<button
key={item.key}
type='button'
onClick={() => setNotificationTab(item.key)}
className={`flex cursor-pointer items-center justify-center gap-1.5 rounded-xl px-3 py-2.5 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70 ${
active
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-950 dark:text-white'
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white'
}`}
>
<Icon className='h-4 w-4' />
<span className='hidden sm:inline'>{item.label}</span>
<span className='sm:hidden'>{item.key === 'email' ? '邮件' : item.key === 'push' ? '浏览器' : 'Telegram'}</span>
</button>
);
})}
</div>
{emailSettingsLoading ? ( {emailSettingsLoading ? (
<div className='space-y-4' aria-live='polite'> <div className='space-y-4' aria-live='polite'>
<div className='animate-pulse rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'> <div className='animate-pulse rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'>
@@ -154,7 +191,9 @@ export function EmailSettingsPanel({
</p> </p>
</div> </div>
) : ( ) : (
<div className='space-y-4'> <>
{/* 邮件通知 */}
{notificationTab === 'email' && (
<section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'> <section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'>
<div className='mb-4 flex items-start gap-3'> <div className='mb-4 flex items-start gap-3'>
<div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-sky-100 text-sky-600 dark:bg-sky-900/30 dark:text-sky-300'> <div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-sky-100 text-sky-600 dark:bg-sky-900/30 dark:text-sky-300'>
@@ -199,7 +238,10 @@ export function EmailSettingsPanel({
/> />
</div> </div>
</section> </section>
)}
{/* 浏览器系统通知 */}
{notificationTab === 'push' && (
<section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'> <section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'>
<div className='mb-4 flex items-start gap-3'> <div className='mb-4 flex items-start gap-3'>
<div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-300'> <div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-300'>
@@ -241,8 +283,12 @@ export function EmailSettingsPanel({
)} )}
</div> </div>
</section> </section>
)}
{telegramEnabled && ( {/* Telegram 通知 */}
{notificationTab === 'telegram' && (
<>
{telegramNotifyEnabled ? (
<section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'> <section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'>
<div className='mb-4 flex items-start gap-3'> <div className='mb-4 flex items-start gap-3'>
<div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-sky-100 text-sky-600 dark:bg-sky-900/30 dark:text-sky-300'> <div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-sky-100 text-sky-600 dark:bg-sky-900/30 dark:text-sky-300'>
@@ -293,6 +339,22 @@ export function EmailSettingsPanel({
)} )}
</div> </div>
</section> </section>
) : (
<section className='rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800'>
<div className='mb-4 flex items-start gap-3'>
<div className='flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'>
<Send className='h-5 w-5' />
</div>
<div className='min-w-0 flex-1'>
<h4 className='text-base font-semibold text-gray-900 dark:text-gray-100'>Telegram Bot </h4>
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
Telegram Bot
</p>
</div>
</div>
</section>
)}
</>
)} )}
<button <button
@@ -322,7 +384,7 @@ export function EmailSettingsPanel({
{statusMessage} {statusMessage}
</p> </p>
) : null} ) : null}
</div> </>
)} )}
<div className='mt-6 flex gap-2 rounded-xl border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20'> <div className='mt-6 flex gap-2 rounded-xl border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20'>