通知增加区域switch,增加免责声明
This commit is contained in:
+222
-3
@@ -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>
|
||||||
|
|||||||
@@ -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,147 +191,172 @@ export function EmailSettingsPanel({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-4'>
|
<>
|
||||||
<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'>
|
{notificationTab === 'email' && (
|
||||||
<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'>
|
|
||||||
<Mail 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'>
|
|
||||||
邮件通知
|
|
||||||
</h4>
|
|
||||||
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
|
||||||
用于接收收藏影视更新等异步提醒,可独立于系统通知关闭。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
|
|
||||||
邮箱地址
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='email'
|
|
||||||
value={userEmail}
|
|
||||||
onChange={(e) => onUserEmailChange(e.target.value)}
|
|
||||||
placeholder='输入您的邮箱地址'
|
|
||||||
disabled={emailSettingsSaving}
|
|
||||||
className='mb-4 w-full rounded-xl border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 transition-colors placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-900 dark:text-white'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between gap-4 rounded-xl bg-white p-3 dark:bg-gray-900/70'>
|
|
||||||
<div>
|
|
||||||
<h5 className='text-sm font-medium text-gray-800 dark:text-gray-200'>
|
|
||||||
收藏更新邮件
|
|
||||||
</h5>
|
|
||||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
|
||||||
当收藏的影片有更新时发送邮件通知。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
checked={emailNotifications}
|
|
||||||
disabled={emailSettingsSaving}
|
|
||||||
label='切换收藏更新邮件通知'
|
|
||||||
onChange={() => onEmailNotificationsChange(!emailNotifications)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-300'>
|
|
||||||
<MonitorSmartphone 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'>
|
|
||||||
当前设备浏览器系统通知
|
|
||||||
</h4>
|
|
||||||
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
|
||||||
当前设备收到站内通知时,通过浏览器推送到系统通知中心。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
checked={pushNotifications}
|
|
||||||
disabled={pushDisabled}
|
|
||||||
busy={pushNotificationsBusy}
|
|
||||||
label='切换当前设备浏览器系统通知'
|
|
||||||
onChange={() => onPushNotificationsChange(!pushNotifications)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-2 rounded-xl bg-white p-3 dark:bg-gray-900/70'>
|
|
||||||
<div className='flex items-center justify-between gap-3 text-sm'>
|
|
||||||
<span className='text-gray-600 dark:text-gray-400'>当前设备</span>
|
|
||||||
<span className={`font-medium ${pushNotificationsSupported ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}`}>
|
|
||||||
{pushNotificationsSupported ? '可用' : '需支持或授权'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{!pushNotificationsConfigured && (
|
|
||||||
<p className='text-xs text-amber-600 dark:text-amber-400' role='alert'>
|
|
||||||
系统正在初始化 Web Push 密钥,请稍后重试。
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{pushNotificationsConfigured && !pushNotificationsSupported && (
|
|
||||||
<p className='text-xs text-amber-600 dark:text-amber-400' role='alert'>
|
|
||||||
当前浏览器、系统权限或登录模式暂不支持系统通知。
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{telegramEnabled && (
|
|
||||||
<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'>
|
||||||
<Send className='h-5 w-5' />
|
<Mail className='h-5 w-5' />
|
||||||
</div>
|
</div>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<h4 className='text-base font-semibold text-gray-900 dark:text-gray-100'>Telegram Bot 通知</h4>
|
<h4 className='text-base font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
邮件通知
|
||||||
|
</h4>
|
||||||
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
||||||
绑定 Telegram 后可接收站内通知,也可在登录页使用 Telegram 确认登录。
|
用于接收收藏影视更新等异步提醒,可独立于系统通知关闭。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-3 rounded-xl bg-white p-3 dark:bg-gray-900/70'>
|
|
||||||
|
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||||
|
邮箱地址
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
value={userEmail}
|
||||||
|
onChange={(e) => onUserEmailChange(e.target.value)}
|
||||||
|
placeholder='输入您的邮箱地址'
|
||||||
|
disabled={emailSettingsSaving}
|
||||||
|
className='mb-4 w-full rounded-xl border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 transition-colors placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-900 dark:text-white'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between gap-4 rounded-xl bg-white p-3 dark:bg-gray-900/70'>
|
||||||
|
<div>
|
||||||
|
<h5 className='text-sm font-medium text-gray-800 dark:text-gray-200'>
|
||||||
|
收藏更新邮件
|
||||||
|
</h5>
|
||||||
|
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
当收藏的影片有更新时发送邮件通知。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={emailNotifications}
|
||||||
|
disabled={emailSettingsSaving}
|
||||||
|
label='切换收藏更新邮件通知'
|
||||||
|
onChange={() => onEmailNotificationsChange(!emailNotifications)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 浏览器系统通知 */}
|
||||||
|
{notificationTab === 'push' && (
|
||||||
|
<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-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-300'>
|
||||||
|
<MonitorSmartphone 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'>
|
||||||
|
当前设备浏览器系统通知
|
||||||
|
</h4>
|
||||||
|
<p className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
当前设备收到站内通知时,通过浏览器推送到系统通知中心。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={pushNotifications}
|
||||||
|
disabled={pushDisabled}
|
||||||
|
busy={pushNotificationsBusy}
|
||||||
|
label='切换当前设备浏览器系统通知'
|
||||||
|
onChange={() => onPushNotificationsChange(!pushNotifications)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-2 rounded-xl bg-white p-3 dark:bg-gray-900/70'>
|
||||||
<div className='flex items-center justify-between gap-3 text-sm'>
|
<div className='flex items-center justify-between gap-3 text-sm'>
|
||||||
<span className='text-gray-600 dark:text-gray-400'>绑定状态</span>
|
<span className='text-gray-600 dark:text-gray-400'>当前设备</span>
|
||||||
<span className={`font-medium ${telegramBound ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}`}>
|
<span className={`font-medium ${pushNotificationsSupported ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}`}>
|
||||||
{telegramBound ? `已绑定${telegramUsername ? ` @${telegramUsername}` : ''}` : '未绑定'}
|
{pushNotificationsSupported ? '可用' : '需支持或授权'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!telegramBound && (
|
{!pushNotificationsConfigured && (
|
||||||
<>
|
<p className='text-xs text-amber-600 dark:text-amber-400' role='alert'>
|
||||||
{telegramBindCode && (
|
系统正在初始化 Web Push 密钥,请稍后重试。
|
||||||
<div className='rounded-lg bg-sky-50 p-3 text-sm text-sky-800 dark:bg-sky-900/30 dark:text-sky-200'>
|
</p>
|
||||||
绑定码:<span className='font-mono text-base font-bold'>{telegramBindCode}</span>
|
)}
|
||||||
<p className='mt-1 text-xs'>在 Bot 中发送 /bind {telegramBindCode},或点击下方按钮打开 Telegram。</p>
|
{pushNotificationsConfigured && !pushNotificationsSupported && (
|
||||||
</div>
|
<p className='text-xs text-amber-600 dark:text-amber-400' role='alert'>
|
||||||
)}
|
当前浏览器、系统权限或登录模式暂不支持系统通知。
|
||||||
<div className='flex gap-2'>
|
</p>
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={onCreateTelegramBindCode}
|
|
||||||
disabled={telegramBindingBusy}
|
|
||||||
className='flex-1 rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-sky-700 disabled:cursor-not-allowed disabled:bg-sky-400'
|
|
||||||
>
|
|
||||||
{telegramBindingBusy ? '生成中...' : '生成绑定码'}
|
|
||||||
</button>
|
|
||||||
{telegramDeepLink && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={() => window.open(telegramDeepLink, '_blank', 'noopener,noreferrer')}
|
|
||||||
className='flex-1 rounded-lg border border-sky-300 px-3 py-2 text-sm font-medium text-sky-700 transition-colors hover:bg-sky-50 dark:border-sky-700 dark:text-sky-300 dark:hover:bg-sky-900/30'
|
|
||||||
>
|
|
||||||
打开 Telegram
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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'>
|
||||||
|
<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'>
|
||||||
|
<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 后可接收站内通知,也可在登录页使用 Telegram 确认登录。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-3 rounded-xl bg-white p-3 dark:bg-gray-900/70'>
|
||||||
|
<div className='flex items-center justify-between gap-3 text-sm'>
|
||||||
|
<span className='text-gray-600 dark:text-gray-400'>绑定状态</span>
|
||||||
|
<span className={`font-medium ${telegramBound ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}`}>
|
||||||
|
{telegramBound ? `已绑定${telegramUsername ? ` @${telegramUsername}` : ''}` : '未绑定'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!telegramBound && (
|
||||||
|
<>
|
||||||
|
{telegramBindCode && (
|
||||||
|
<div className='rounded-lg bg-sky-50 p-3 text-sm text-sky-800 dark:bg-sky-900/30 dark:text-sky-200'>
|
||||||
|
绑定码:<span className='font-mono text-base font-bold'>{telegramBindCode}</span>
|
||||||
|
<p className='mt-1 text-xs'>在 Bot 中发送 /bind {telegramBindCode},或点击下方按钮打开 Telegram。</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={onCreateTelegramBindCode}
|
||||||
|
disabled={telegramBindingBusy}
|
||||||
|
className='flex-1 rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-sky-700 disabled:cursor-not-allowed disabled:bg-sky-400'
|
||||||
|
>
|
||||||
|
{telegramBindingBusy ? '生成中...' : '生成绑定码'}
|
||||||
|
</button>
|
||||||
|
{telegramDeepLink && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => window.open(telegramDeepLink, '_blank', 'noopener,noreferrer')}
|
||||||
|
className='flex-1 rounded-lg border border-sky-300 px-3 py-2 text-sm font-medium text-sky-700 transition-colors hover:bg-sky-50 dark:border-sky-700 dark:text-sky-300 dark:hover:bg-sky-900/30'
|
||||||
|
>
|
||||||
|
打开 Telegram
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={emailSettingsSaving}
|
disabled={emailSettingsSaving}
|
||||||
@@ -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'>
|
||||||
|
|||||||
Reference in New Issue
Block a user