Add isolated Pages demo mode with sample vault data

This commit is contained in:
shuaiplus
2026-05-04 21:09:10 +08:00
parent ba38b77387
commit 70dc9a76a9
16 changed files with 1574 additions and 84 deletions
+9 -1
View File
@@ -19,6 +19,9 @@ interface RegisterValues {
interface AuthViewsProps {
mode: 'login' | 'register' | 'locked';
relaxedLoginInput?: boolean;
authPlaceholder?: string;
unlockPlaceholder?: string;
pendingAction: 'login' | 'register' | 'unlock' | null;
unlockReady: boolean;
unlockPreparing: boolean;
@@ -46,6 +49,7 @@ function PasswordField(props: {
onInput: (v: string) => void;
autoFocus?: boolean;
autoComplete?: string;
placeholder?: string;
}) {
const [show, setShow] = useState(false);
return (
@@ -59,6 +63,7 @@ function PasswordField(props: {
onInput={(e) => props.onInput((e.currentTarget as HTMLInputElement).value)}
autoFocus={props.autoFocus}
autoComplete={props.autoComplete}
placeholder={props.placeholder}
/>
<button type="button" className="eye-btn" onClick={() => setShow((v) => !v)}>
{show ? <EyeOff size={16} /> : <Eye size={16} />}
@@ -90,6 +95,7 @@ export default function AuthViews(props: AuthViewsProps) {
value={props.unlockPassword}
autoFocus
autoComplete="current-password"
placeholder={props.unlockPlaceholder}
onInput={props.onChangeUnlock}
/>
<div className="auth-support-row">
@@ -217,9 +223,10 @@ export default function AuthViews(props: AuthViewsProps) {
<span>{t('txt_email')}</span>
<input
className="input"
type="email"
type={props.relaxedLoginInput ? 'text' : 'email'}
value={props.loginValues.email}
autoComplete="username"
placeholder={props.authPlaceholder}
onInput={(e) => props.onChangeLogin({ ...props.loginValues, email: (e.currentTarget as HTMLInputElement).value })}
/>
</label>
@@ -227,6 +234,7 @@ export default function AuthViews(props: AuthViewsProps) {
label={t('txt_master_password')}
value={props.loginValues.password}
autoComplete="current-password"
placeholder={props.authPlaceholder}
onInput={(v) => props.onChangeLogin({ ...props.loginValues, password: v })}
autoFocus
/>
+17
View File
@@ -6,6 +6,7 @@ import { toBufferSource } from '@/lib/crypto';
import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download';
import NotFoundPage from '@/components/NotFoundPage';
import StandalonePageFrame from '@/components/StandalonePageFrame';
import { getDemoPublicSend, IS_DEMO_MODE } from '@/lib/demo';
import { t } from '@/lib/i18n';
interface PublicSendPageProps {
@@ -108,6 +109,17 @@ export default function PublicSendPage(props: PublicSendPageProps) {
setNotFound(false);
setLoading(true);
try {
if (IS_DEMO_MODE) {
const demoSend = getDemoPublicSend(props.accessId);
if (!demoSend) {
setNotFound(true);
setSendData(null);
return;
}
setSendData(demoSend);
setNeedPassword(false);
return;
}
if (!hasUsableSendKey(props.keyPart)) {
setNotFound(true);
setSendData(null);
@@ -153,6 +165,11 @@ export default function PublicSendPage(props: PublicSendPageProps) {
setDownloadPercent(null);
setError('');
try {
if (IS_DEMO_MODE) {
const bytes = new TextEncoder().encode('NodeWarden demo file Send.\nThis download is generated locally in demo mode.\n');
downloadBytesAsFile(bytes, sendData.decFileName || sendData.file?.fileName || 'nodewarden-demo-send.txt', 'application/octet-stream');
return;
}
const url = await accessPublicSendFile(sendData.id, sendData.file.id, props.keyPart, password || undefined);
const resp = await fetch(url);
if (!resp.ok) throw new Error(t('txt_download_failed'));
+33 -1
View File
@@ -11,6 +11,7 @@ import {
import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils';
const ICON_LOAD_ROOT_MARGIN = '180px 0px';
const SHOULD_LOAD_DEMO_BRAND_ICONS = __NODEWARDEN_DEMO__;
interface WebsiteIconProps {
cipher: Cipher;
@@ -24,6 +25,21 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true));
const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle'));
const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : ''));
const [demoIconUrl, setDemoIconUrl] = useState('');
useEffect(() => {
if (!SHOULD_LOAD_DEMO_BRAND_ICONS || !host) {
setDemoIconUrl('');
return;
}
let disposed = false;
void import('@/lib/demo-brand-icons').then(({ demoBrandIconUrl }) => {
if (!disposed) setDemoIconUrl(demoBrandIconUrl(host));
});
return () => {
disposed = true;
};
}, [host]);
useEffect(() => {
if (!host) {
@@ -72,6 +88,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
}, [host, shouldLoad, status]);
useEffect(() => {
if (demoIconUrl) return;
if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return;
let disposed = false;
void preloadWebsiteIcon(host, src).then((nextStatus) => {
@@ -82,7 +99,21 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
return () => {
disposed = true;
};
}, [host, src, shouldLoad, status]);
}, [demoIconUrl, host, src, shouldLoad, status]);
if (demoIconUrl) {
return (
<span className="list-icon-stack" ref={nodeRef}>
<img
className="list-icon loaded"
src={demoIconUrl}
alt=""
loading="lazy"
decoding="async"
/>
</span>
);
}
if (!host || status === 'error') {
return <span className="list-icon-fallback">{props.fallback ?? <Globe size={18} />}</span>;
@@ -103,3 +134,4 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
</span>
);
}