feat: list and reorder logged-in accounts in settings #282
This commit is contained in:
@@ -161,6 +161,7 @@ const tabSearchPaths: Record<Tab, string[]> = {
|
|||||||
'settings.account.email',
|
'settings.account.email',
|
||||||
'settings.account.server',
|
'settings.account.server',
|
||||||
'settings.account.storage',
|
'settings.account.storage',
|
||||||
|
'settings.account.accounts',
|
||||||
],
|
],
|
||||||
language: ['settings.appearance.language'],
|
language: ['settings.appearance.language'],
|
||||||
notifications: ['settings.notifications'],
|
notifications: ['settings.notifications'],
|
||||||
@@ -228,7 +229,7 @@ const tabSearchPaths: Record<Tab, string[]> = {
|
|||||||
// Extra English keywords per tab so common search terms hit even when the
|
// Extra English keywords per tab so common search terms hit even when the
|
||||||
// translation doesn't contain the literal word.
|
// translation doesn't contain the literal word.
|
||||||
const tabKeywords: Record<Tab, string> = {
|
const tabKeywords: Record<Tab, string> = {
|
||||||
account: 'profile email password user signin signout',
|
account: 'profile email password user signin signout reorder rearrange drag dropdown switcher multi-account',
|
||||||
language: 'locale region timezone date time format',
|
language: 'locale region timezone date time format',
|
||||||
notifications: 'sound alert push badge',
|
notifications: 'sound alert push badge',
|
||||||
appearance: 'theme dark light font size accent color animation density',
|
appearance: 'theme dark light font size accent color animation density',
|
||||||
|
|||||||
@@ -1,87 +1,361 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useCallback } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { Check, GripVertical, Plus, Star, AlertCircle } from 'lucide-react';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { useEmailStore } from '@/stores/email-store';
|
import { useEmailStore } from '@/stores/email-store';
|
||||||
import { useAccountStore } from '@/stores/account-store';
|
import { useAccountStore, type AccountEntry } from '@/stores/account-store';
|
||||||
import { SettingsSection, SettingItem } from './settings-section';
|
import { SettingsSection, SettingItem } from './settings-section';
|
||||||
import { formatFileSize } from '@/lib/utils';
|
import { Avatar } from '@/components/ui/avatar';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useRouter } from '@/i18n/navigation';
|
||||||
|
import { getMaxAccounts } from '@/lib/account-utils';
|
||||||
|
import { formatFileSize, cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
function hostnameOf(serverUrl: string): string {
|
||||||
|
try { return new URL(serverUrl).hostname; } catch { return serverUrl; }
|
||||||
|
}
|
||||||
|
|
||||||
export function AccountSettings() {
|
export function AccountSettings() {
|
||||||
const t = useTranslations('settings.account');
|
const t = useTranslations('settings.account');
|
||||||
const { username, serverUrl, isDemoMode, primaryIdentity, authMode, activeAccountId } = useAuthStore();
|
const router = useRouter();
|
||||||
|
const { username, serverUrl, isDemoMode, primaryIdentity, authMode } = useAuthStore();
|
||||||
|
const activeAccountId = useAuthStore((s) => s.activeAccountId);
|
||||||
|
const switchAccount = useAuthStore((s) => s.switchAccount);
|
||||||
const { quota } = useEmailStore();
|
const { quota } = useEmailStore();
|
||||||
|
const accounts = useAccountStore((s) => s.accounts);
|
||||||
|
const setDefaultAccount = useAccountStore((s) => s.setDefaultAccount);
|
||||||
|
const reorderAccounts = useAccountStore((s) => s.reorderAccounts);
|
||||||
const account = useAccountStore((s) => activeAccountId ? s.getAccountById(activeAccountId) : undefined);
|
const account = useAccountStore((s) => activeAccountId ? s.getAccountById(activeAccountId) : undefined);
|
||||||
|
|
||||||
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||||
|
const draggedIndexRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const quotaPercentage = quota ? Math.round((quota.used / quota.total) * 100) : 0;
|
const quotaPercentage = quota ? Math.round((quota.used / quota.total) * 100) : 0;
|
||||||
const displayName = primaryIdentity?.name || account?.displayName || (isDemoMode ? 'Demo User' : undefined);
|
const displayName = primaryIdentity?.name || account?.displayName || (isDemoMode ? 'Demo User' : undefined);
|
||||||
const email = primaryIdentity?.email || account?.email || username;
|
const email = primaryIdentity?.email || account?.email || username;
|
||||||
|
const max = getMaxAccounts();
|
||||||
|
|
||||||
|
const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
|
||||||
|
draggedIndexRef.current = index;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', String(index));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
setDragOverIndex(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e: React.DragEvent, dropIndex: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragOverIndex(null);
|
||||||
|
const fromIndex = draggedIndexRef.current;
|
||||||
|
if (fromIndex === null || fromIndex === dropIndex) return;
|
||||||
|
const next = accounts.map((a) => a.id);
|
||||||
|
const [moved] = next.splice(fromIndex, 1);
|
||||||
|
next.splice(dropIndex, 0, moved);
|
||||||
|
reorderAccounts(next);
|
||||||
|
}, [accounts, reorderAccounts]);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
draggedIndexRef.current = null;
|
||||||
|
setDragOverIndex(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const moveAccount = useCallback((from: number, to: number) => {
|
||||||
|
if (to < 0 || to >= accounts.length || from === to) return;
|
||||||
|
const next = accounts.map((a) => a.id);
|
||||||
|
const [moved] = next.splice(from, 1);
|
||||||
|
next.splice(to, 0, moved);
|
||||||
|
reorderAccounts(next);
|
||||||
|
}, [accounts, reorderAccounts]);
|
||||||
|
|
||||||
|
const handleSwitch = useCallback((id: string) => {
|
||||||
|
if (id === activeAccountId) return;
|
||||||
|
void switchAccount(id);
|
||||||
|
}, [activeAccountId, switchAccount]);
|
||||||
|
|
||||||
|
const handleAddAccount = useCallback(() => {
|
||||||
|
router.push(`/login?mode=add-account` as never);
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('title')} description={t('description')}>
|
<div className="space-y-8">
|
||||||
{/* Display Name */}
|
<SettingsSection title={t('title')} description={t('description')}>
|
||||||
<SettingItem label={t('name_label')}>
|
{/* Display Name */}
|
||||||
<span className="text-sm text-foreground">{displayName || t('../../common.unknown')}</span>
|
<SettingItem label={t('name_label')}>
|
||||||
</SettingItem>
|
<span className="text-sm text-foreground">{displayName || t('../../common.unknown')}</span>
|
||||||
|
|
||||||
{/* Email Address */}
|
|
||||||
<SettingItem label={t('email.label')}>
|
|
||||||
<span className="text-sm text-foreground">{email || t('../../common.unknown')}</span>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
{/* Username / Login (show when it differs from email) */}
|
|
||||||
{username && username !== email && (
|
|
||||||
<SettingItem label={t('username_label')}>
|
|
||||||
<span className="text-sm text-foreground">{username}</span>
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Authentication Method */}
|
{/* Email Address */}
|
||||||
<SettingItem label={t('auth_method_label')}>
|
<SettingItem label={t('email.label')}>
|
||||||
<span className="text-sm text-foreground">
|
<span className="text-sm text-foreground">{email || t('../../common.unknown')}</span>
|
||||||
{authMode === 'oauth' ? t('auth_method_oauth') : t('auth_method_basic')}
|
|
||||||
</span>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
{/* Server */}
|
|
||||||
<SettingItem label={t('server.label')}>
|
|
||||||
<span className="text-sm text-foreground truncate max-w-xs">
|
|
||||||
{serverUrl || t('../../common.unknown')}
|
|
||||||
</span>
|
|
||||||
</SettingItem>
|
|
||||||
|
|
||||||
{/* Storage */}
|
|
||||||
{quota && quota.total > 0 && (
|
|
||||||
<SettingItem
|
|
||||||
label={t('storage.label')}
|
|
||||||
description={t('storage.used', {
|
|
||||||
used: formatFileSize(quota.used),
|
|
||||||
total: formatFileSize(quota.total),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-end gap-1">
|
|
||||||
<span className="text-sm text-foreground">
|
|
||||||
{t('storage.percentage', { percent: quotaPercentage })}
|
|
||||||
</span>
|
|
||||||
<div className="w-32 h-2 bg-muted rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-primary rounded-full transition-all"
|
|
||||||
style={{ width: `${quotaPercentage}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Demo mode indicator */}
|
{/* Username / Login (show when it differs from email) */}
|
||||||
{isDemoMode && (
|
{username && username !== email && (
|
||||||
<SettingItem label={t('account_type_label')}>
|
<SettingItem label={t('username_label')}>
|
||||||
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-amber-600 dark:text-amber-400">
|
<span className="text-sm text-foreground">{username}</span>
|
||||||
<span className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
|
</SettingItem>
|
||||||
{t('demo_account')}
|
)}
|
||||||
|
|
||||||
|
{/* Authentication Method */}
|
||||||
|
<SettingItem label={t('auth_method_label')}>
|
||||||
|
<span className="text-sm text-foreground">
|
||||||
|
{authMode === 'oauth' ? t('auth_method_oauth') : t('auth_method_basic')}
|
||||||
</span>
|
</span>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Server */}
|
||||||
|
<SettingItem label={t('server.label')}>
|
||||||
|
<span className="text-sm text-foreground truncate max-w-xs">
|
||||||
|
{serverUrl || t('../../common.unknown')}
|
||||||
|
</span>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Storage */}
|
||||||
|
{quota && quota.total > 0 && (
|
||||||
|
<SettingItem
|
||||||
|
label={t('storage.label')}
|
||||||
|
description={t('storage.used', {
|
||||||
|
used: formatFileSize(quota.used),
|
||||||
|
total: formatFileSize(quota.total),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-end gap-1">
|
||||||
|
<span className="text-sm text-foreground">
|
||||||
|
{t('storage.percentage', { percent: quotaPercentage })}
|
||||||
|
</span>
|
||||||
|
<div className="w-32 h-2 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary rounded-full transition-all"
|
||||||
|
style={{ width: `${quotaPercentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Demo mode indicator */}
|
||||||
|
{isDemoMode && (
|
||||||
|
<SettingItem label={t('account_type_label')}>
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-amber-600 dark:text-amber-400">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
|
||||||
|
{t('demo_account')}
|
||||||
|
</span>
|
||||||
|
</SettingItem>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{/* Logged-in accounts list */}
|
||||||
|
{accounts.length > 0 && (
|
||||||
|
<SettingsSection title={t('accounts.title')} description={t('accounts.description')}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{accounts.map((a, index) => (
|
||||||
|
<AccountRow
|
||||||
|
key={a.id}
|
||||||
|
account={a}
|
||||||
|
index={index}
|
||||||
|
isActive={a.id === activeAccountId}
|
||||||
|
isFirst={index === 0}
|
||||||
|
isLast={index === accounts.length - 1}
|
||||||
|
isDragOver={dragOverIndex === index}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onMoveUp={() => moveAccount(index, index - 1)}
|
||||||
|
onMoveDown={() => moveAccount(index, index + 1)}
|
||||||
|
onSwitch={() => handleSwitch(a.id)}
|
||||||
|
onSetDefault={() => setDefaultAccount(a.id)}
|
||||||
|
labels={{
|
||||||
|
active: t('accounts.active'),
|
||||||
|
default: t('accounts.default_badge'),
|
||||||
|
setDefault: t('accounts.set_default'),
|
||||||
|
switchTo: t('accounts.switch_to'),
|
||||||
|
moveUp: t('accounts.move_up'),
|
||||||
|
moveDown: t('accounts.move_down'),
|
||||||
|
dragHandle: t('accounts.drag_handle'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{accounts.length < max && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAddAccount}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
{t('accounts.add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
</SettingsSection>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountRowProps {
|
||||||
|
account: AccountEntry;
|
||||||
|
index: number;
|
||||||
|
isActive: boolean;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
isDragOver: boolean;
|
||||||
|
onDragStart: (e: React.DragEvent, index: number) => void;
|
||||||
|
onDragOver: (e: React.DragEvent, index: number) => void;
|
||||||
|
onDrop: (e: React.DragEvent, index: number) => void;
|
||||||
|
onDragEnd: () => void;
|
||||||
|
onMoveUp: () => void;
|
||||||
|
onMoveDown: () => void;
|
||||||
|
onSwitch: () => void;
|
||||||
|
onSetDefault: () => void;
|
||||||
|
labels: {
|
||||||
|
active: string;
|
||||||
|
default: string;
|
||||||
|
setDefault: string;
|
||||||
|
switchTo: string;
|
||||||
|
moveUp: string;
|
||||||
|
moveDown: string;
|
||||||
|
dragHandle: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountRow({
|
||||||
|
account,
|
||||||
|
index,
|
||||||
|
isActive,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
isDragOver,
|
||||||
|
onDragStart,
|
||||||
|
onDragOver,
|
||||||
|
onDrop,
|
||||||
|
onDragEnd,
|
||||||
|
onMoveUp,
|
||||||
|
onMoveDown,
|
||||||
|
onSwitch,
|
||||||
|
onSetDefault,
|
||||||
|
labels,
|
||||||
|
}: AccountRowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => onDragStart(e, index)}
|
||||||
|
onDragOver={(e) => onDragOver(e, index)}
|
||||||
|
onDrop={(e) => onDrop(e, index)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 border rounded-lg transition-colors',
|
||||||
|
isDragOver
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: isActive
|
||||||
|
? 'border-border bg-accent/30'
|
||||||
|
: 'border-border hover:bg-muted/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground flex-shrink-0"
|
||||||
|
title={labels.dragHandle}
|
||||||
|
>
|
||||||
|
<GripVertical className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<Avatar
|
||||||
|
name={account.displayName || account.label}
|
||||||
|
email={account.email || account.username}
|
||||||
|
size="sm"
|
||||||
|
className="w-9 h-9 text-sm"
|
||||||
|
disableFavicon
|
||||||
|
fallbackColor={account.avatarColor}
|
||||||
|
/>
|
||||||
|
{isActive && (
|
||||||
|
<div className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full bg-primary flex items-center justify-center">
|
||||||
|
<Check className="w-2.5 h-2.5 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSwitch}
|
||||||
|
disabled={isActive}
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 flex-1 text-left',
|
||||||
|
!isActive && 'cursor-pointer'
|
||||||
|
)}
|
||||||
|
title={isActive ? labels.active : labels.switchTo}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-sm font-medium truncate">
|
||||||
|
{account.displayName || account.label}
|
||||||
|
</span>
|
||||||
|
{account.isDefault && (
|
||||||
|
<Star className="w-3 h-3 text-amber-500 flex-shrink-0 fill-amber-500" aria-label={labels.default} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{account.email || account.username}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1 mt-0.5">
|
||||||
|
{account.hasError ? (
|
||||||
|
<AlertCircle className="w-3 h-3 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<span className={cn(
|
||||||
|
'w-1.5 h-1.5 rounded-full',
|
||||||
|
account.isConnected ? 'bg-green-500' : 'bg-muted-foreground/40'
|
||||||
|
)} />
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-muted-foreground truncate">
|
||||||
|
{hostnameOf(account.serverUrl)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||||
|
{!account.isDefault && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSetDefault}
|
||||||
|
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-amber-500 transition-colors"
|
||||||
|
title={labels.setDefault}
|
||||||
|
aria-label={labels.setDefault}
|
||||||
|
>
|
||||||
|
<Star className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onMoveUp}
|
||||||
|
disabled={isFirst}
|
||||||
|
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:cursor-not-allowed"
|
||||||
|
title={labels.moveUp}
|
||||||
|
aria-label={labels.moveUp}
|
||||||
|
>
|
||||||
|
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 10l4-4 4 4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onMoveDown}
|
||||||
|
disabled={isLast}
|
||||||
|
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:cursor-not-allowed"
|
||||||
|
title={labels.moveDown}
|
||||||
|
aria-label={labels.moveDown}
|
||||||
|
>
|
||||||
|
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<path d="M4 6l4 4 4-4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1199,6 +1199,18 @@
|
|||||||
"last_sync": {
|
"last_sync": {
|
||||||
"label": "Letzte Synchronisierung",
|
"label": "Letzte Synchronisierung",
|
||||||
"value": "{time}"
|
"value": "{time}"
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"title": "Angemeldete Konten",
|
||||||
|
"description": "Ziehen Sie zum Sortieren, wie Konten im Kontomenü erscheinen",
|
||||||
|
"active": "Aktuell aktives Konto",
|
||||||
|
"default_badge": "Standardkonto",
|
||||||
|
"set_default": "Als Standard festlegen",
|
||||||
|
"switch_to": "Zu diesem Konto wechseln",
|
||||||
|
"move_up": "Nach oben",
|
||||||
|
"move_down": "Nach unten",
|
||||||
|
"drag_handle": "Zum Sortieren ziehen",
|
||||||
|
"add": "Konto hinzufügen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -1202,6 +1202,18 @@
|
|||||||
"last_sync": {
|
"last_sync": {
|
||||||
"label": "Last Sync",
|
"label": "Last Sync",
|
||||||
"value": "{time}"
|
"value": "{time}"
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"title": "Logged-in accounts",
|
||||||
|
"description": "Drag to reorder how accounts appear in the account dropdown",
|
||||||
|
"active": "Currently active account",
|
||||||
|
"default_badge": "Default account",
|
||||||
|
"set_default": "Set as default",
|
||||||
|
"switch_to": "Switch to this account",
|
||||||
|
"move_up": "Move up",
|
||||||
|
"move_down": "Move down",
|
||||||
|
"drag_handle": "Drag to reorder",
|
||||||
|
"add": "Add account"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ interface AccountState {
|
|||||||
setDefaultAccount: (accountId: string) => void;
|
setDefaultAccount: (accountId: string) => void;
|
||||||
getDefaultAccount: () => AccountEntry | null;
|
getDefaultAccount: () => AccountEntry | null;
|
||||||
updateAccount: (accountId: string, updates: Partial<AccountEntry>) => void;
|
updateAccount: (accountId: string, updates: Partial<AccountEntry>) => void;
|
||||||
|
reorderAccounts: (orderedIds: string[]) => void;
|
||||||
getActiveAccount: () => AccountEntry | null;
|
getActiveAccount: () => AccountEntry | null;
|
||||||
getAccountById: (accountId: string) => AccountEntry | undefined;
|
getAccountById: (accountId: string) => AccountEntry | undefined;
|
||||||
getNextCookieSlot: () => number;
|
getNextCookieSlot: () => number;
|
||||||
@@ -168,6 +169,23 @@ export const useAccountStore = create<AccountState>()(
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reorderAccounts: (orderedIds) => {
|
||||||
|
set((s) => {
|
||||||
|
const byId = new Map(s.accounts.map((a) => [a.id, a]));
|
||||||
|
const reordered: AccountEntry[] = [];
|
||||||
|
for (const id of orderedIds) {
|
||||||
|
const a = byId.get(id);
|
||||||
|
if (a) {
|
||||||
|
reordered.push(a);
|
||||||
|
byId.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Append any accounts that weren't in the ordered list (defensive)
|
||||||
|
for (const a of byId.values()) reordered.push(a);
|
||||||
|
return { accounts: reordered };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getActiveAccount: () => {
|
getActiveAccount: () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
return state.accounts.find((a) => a.id === state.activeAccountId) ?? null;
|
return state.accounts.find((a) => a.id === state.activeAccountId) ?? null;
|
||||||
|
|||||||
Reference in New Issue
Block a user