mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Polish vault icons and mobile layout
This commit is contained in:
@@ -36,6 +36,7 @@ interface VaultPageProps {
|
||||
ciphers: Cipher[];
|
||||
folders: Folder[];
|
||||
loading: boolean;
|
||||
error: string;
|
||||
emailForReprompt: string;
|
||||
onRefresh: () => Promise<void>;
|
||||
onCreate: (draft: VaultDraft, attachments?: File[]) => Promise<void>;
|
||||
@@ -1021,6 +1022,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
||||
<VaultListPanel
|
||||
busy={busy}
|
||||
loading={props.loading}
|
||||
error={props.error}
|
||||
searchInput={searchInput}
|
||||
sortMode={sortMode}
|
||||
sortMenuOpen={sortMenuOpen}
|
||||
@@ -1140,7 +1142,20 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEditing && !selectedCipher && (props.loading ? <LoadingState card lines={5} /> : <div className="empty card">{t('txt_select_an_item')}</div>)}
|
||||
{!isEditing && !selectedCipher && (
|
||||
props.loading
|
||||
? <LoadingState card lines={5} />
|
||||
: props.error
|
||||
? (
|
||||
<div className="empty card vault-error-state">
|
||||
<strong>{props.error}</strong>
|
||||
<button type="button" className="btn btn-secondary small" disabled={busy} onClick={handleSyncVault}>
|
||||
{t('txt_retry_sync')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: <div className="empty card">{t('txt_select_an_item')}</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createPortal } from 'preact/compat';
|
||||
import { useMemo, useState } from 'preact/hooks';
|
||||
import { Archive, Clipboard, Download, Eye, EyeOff, ExternalLink, Paperclip, Pencil, RotateCcw, Trash2, X } from 'lucide-preact';
|
||||
import { Archive, Clipboard, Download, Eye, EyeOff, ExternalLink, Folder, Paperclip, Pencil, RotateCcw, Trash2, X } from 'lucide-preact';
|
||||
import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
||||
import type { Cipher } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import {
|
||||
TOTP_PERIOD_SECONDS,
|
||||
TOTP_RING_CIRCUMFERENCE,
|
||||
VaultListIcon,
|
||||
copyToClipboard,
|
||||
formatAttachmentSize,
|
||||
formatHistoryTime,
|
||||
@@ -115,8 +116,18 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
||||
{(Number(props.selectedCipher.reprompt || 0) !== 1 || props.repromptApprovedCipherId === props.selectedCipher.id) && (
|
||||
<>
|
||||
<div className="card">
|
||||
<h3 className="detail-title">{props.selectedCipher.decName || t('txt_no_name')}</h3>
|
||||
<div className="detail-sub">{props.folderName(props.selectedCipher.folderId)}</div>
|
||||
<div className="detail-title-row">
|
||||
<span className="detail-title-icon" aria-hidden="true">
|
||||
<VaultListIcon cipher={props.selectedCipher} />
|
||||
</span>
|
||||
<div className="detail-title-main">
|
||||
<h3 className="detail-title">{props.selectedCipher.decName || t('txt_no_name')}</h3>
|
||||
<div className="detail-folder-line">
|
||||
<Folder size={13} aria-hidden="true" />
|
||||
<span>{props.folderName(props.selectedCipher.folderId)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isArchived && <div className="list-badge archive-badge">{t('txt_archived')}</div>}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ interface VirtualRange {
|
||||
interface VaultListPanelProps {
|
||||
busy: boolean;
|
||||
loading: boolean;
|
||||
error: string;
|
||||
searchInput: string;
|
||||
sortMode: VaultSortMode;
|
||||
sortMenuOpen: boolean;
|
||||
@@ -238,6 +239,14 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
||||
|
||||
<div className="list-panel" ref={props.listPanelRef} onScroll={(event) => props.onScroll((event.currentTarget as HTMLDivElement).scrollTop)}>
|
||||
{props.loading && !props.filteredCiphers.length && <LoadingState lines={7} compact />}
|
||||
{!props.loading && !!props.error && !props.filteredCiphers.length && (
|
||||
<div className="empty vault-error-state">
|
||||
<strong>{props.error}</strong>
|
||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onSyncVault}>
|
||||
{t('txt_retry_sync')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!!props.filteredCiphers.length && (
|
||||
<div style={{ paddingTop: `${props.virtualRange.padTop}px`, paddingBottom: `${props.virtualRange.padBottom}px` }}>
|
||||
{props.visibleCiphers.map((cipher) => (
|
||||
@@ -253,7 +262,7 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!props.loading && !props.filteredCiphers.length && <div className="empty">{t('txt_no_items')}</div>}
|
||||
{!props.loading && !props.error && !props.filteredCiphers.length && <div className="empty">{t('txt_no_items')}</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,8 @@ import type { ComponentChildren } from 'preact';
|
||||
import { Globe } from 'lucide-preact';
|
||||
import type { Cipher } from '@/lib/types';
|
||||
import {
|
||||
getWebsiteIconImageUrl,
|
||||
getWebsiteIconStatus,
|
||||
markWebsiteIconErrored,
|
||||
markWebsiteIconLoaded,
|
||||
preloadWebsiteIcon,
|
||||
subscribeWebsiteIconStatus,
|
||||
} from '@/lib/website-icon-cache';
|
||||
@@ -24,17 +23,23 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
||||
const nodeRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true));
|
||||
const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle'));
|
||||
const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : ''));
|
||||
|
||||
useEffect(() => {
|
||||
if (!host) {
|
||||
setShouldLoad(true);
|
||||
setStatus('idle');
|
||||
setImageUrl('');
|
||||
return;
|
||||
}
|
||||
const nextStatus = getWebsiteIconStatus(host);
|
||||
setShouldLoad(nextStatus === 'loaded');
|
||||
setStatus(nextStatus);
|
||||
return subscribeWebsiteIconStatus(host, setStatus);
|
||||
setImageUrl(getWebsiteIconImageUrl(host));
|
||||
return subscribeWebsiteIconStatus(host, (next) => {
|
||||
setStatus(next);
|
||||
setImageUrl(getWebsiteIconImageUrl(host));
|
||||
});
|
||||
}, [host]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -70,7 +75,9 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
||||
if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return;
|
||||
let disposed = false;
|
||||
void preloadWebsiteIcon(host, src).then((nextStatus) => {
|
||||
if (!disposed) setStatus(nextStatus);
|
||||
if (disposed) return;
|
||||
setStatus(nextStatus);
|
||||
setImageUrl(getWebsiteIconImageUrl(host));
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
@@ -84,16 +91,13 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
||||
return (
|
||||
<span className="list-icon-stack" ref={nodeRef}>
|
||||
{status !== 'loaded' && <span className="list-icon-fallback">{props.fallback ?? <Globe size={18} />}</span>}
|
||||
{status === 'loaded' && (
|
||||
{status === 'loaded' && imageUrl && (
|
||||
<img
|
||||
className="list-icon loaded"
|
||||
src={src}
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerPolicy="no-referrer"
|
||||
onLoad={() => markWebsiteIconLoaded(host)}
|
||||
onError={() => markWebsiteIconErrored(host)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user