feat: add fullscreen layout support with toggle and localization updates

This commit is contained in:
shuaiplus
2026-06-26 11:26:02 +08:00
parent ff85698edb
commit d722815999
3 changed files with 65 additions and 15 deletions
+11 -2
View File
@@ -56,6 +56,7 @@ type PendingRestoreIntegrity =
type PendingBackupVerification = type PendingBackupVerification =
| { action: 'export' } | { action: 'export' }
| { action: 'saveSettings' } | { action: 'saveSettings' }
| { action: 'deleteDestination'; destinationId: string; settings: AdminBackupSettings }
| { action: 'import'; replaceExisting: boolean; allowChecksumMismatch: boolean; knownIntegrity?: BackupFileIntegrityCheckResult } | { action: 'import'; replaceExisting: boolean; allowChecksumMismatch: boolean; knownIntegrity?: BackupFileIntegrityCheckResult }
| { action: 'runRemoteBackup' } | { action: 'runRemoteBackup' }
| { action: 'downloadRemote'; path: string } | { action: 'downloadRemote'; path: string }
@@ -240,7 +241,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
const backupPasswordPromptTitle = const backupPasswordPromptTitle =
pendingBackupVerification?.action === 'export' pendingBackupVerification?.action === 'export'
? t('txt_backup_export') ? t('txt_backup_export')
: pendingBackupVerification?.action === 'saveSettings' : pendingBackupVerification?.action === 'saveSettings' || pendingBackupVerification?.action === 'deleteDestination'
? t('txt_backup_save_settings') ? t('txt_backup_save_settings')
: pendingBackupVerification?.action === 'runRemoteBackup' : pendingBackupVerification?.action === 'runRemoteBackup'
? t('txt_backup_run_manual') ? t('txt_backup_run_manual')
@@ -501,10 +502,16 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
destinations: (savedSettings?.destinations || []).filter((destination) => destination.id !== destinationIdToDelete), destinations: (savedSettings?.destinations || []).filter((destination) => destination.id !== destinationIdToDelete),
}; };
setPendingBackupVerification({ action: 'deleteDestination', destinationId: destinationIdToDelete, settings: nextSettings });
setBackupPasswordValue('');
setConfirmDeleteDestinationOpen(false);
}
async function executeDeleteDestination(masterPassword: string, destinationIdToDelete: string, payload: AdminBackupSettings) {
setSavingSettings(true); setSavingSettings(true);
setLocalError(''); setLocalError('');
try { try {
const saved = await props.onSaveSettings(nextSettings); const saved = await props.onSaveSettings(masterPassword, payload);
const nextDraftDestinations = settings.destinations.filter((destination) => destination.id !== destinationIdToDelete); const nextDraftDestinations = settings.destinations.filter((destination) => destination.id !== destinationIdToDelete);
const nextSelected = getFirstVisibleDestinationId({ destinations: nextDraftDestinations }) || getFirstVisibleDestinationId(saved); const nextSelected = getFirstVisibleDestinationId({ destinations: nextDraftDestinations }) || getFirstVisibleDestinationId(saved);
setSavedSettings(saved); setSavedSettings(saved);
@@ -865,6 +872,8 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
await executeExport(masterPassword); await executeExport(masterPassword);
} else if (request.action === 'saveSettings') { } else if (request.action === 'saveSettings') {
await executeSaveSettings(masterPassword); await executeSaveSettings(masterPassword);
} else if (request.action === 'deleteDestination') {
await executeDeleteDestination(masterPassword, request.destinationId, request.settings);
} else if (request.action === 'import') { } else if (request.action === 'import') {
await executeLocalRestore(masterPassword, request.replaceExisting, request.allowChecksumMismatch, request.knownIntegrity); await executeLocalRestore(masterPassword, request.replaceExisting, request.allowChecksumMismatch, request.knownIntegrity);
} else if (request.action === 'runRemoteBackup') { } else if (request.action === 'runRemoteBackup') {
+10 -1
View File
@@ -234,11 +234,19 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
const normalizedName = String(name || '').trim() || t('txt_account_passkey'); const normalizedName = String(name || '').trim() || t('txt_account_passkey');
const derived = await deriveLoginHash(profile.email, normalizedPassword, defaultKdfIterations); const derived = await deriveLoginHash(profile.email, normalizedPassword, defaultKdfIterations);
const options = await getAccountPasskeyAttestationOptions(authedFetch, derived.hash); const options = await getAccountPasskeyAttestationOptions(authedFetch, derived.hash);
const pending = await createAccountPasskeyCredential(options); const pending = await createAccountPasskeyCredential(options, directUnlock);
let keySet = null; let keySet = null;
let savedWithoutDirectUnlock = false; let savedWithoutDirectUnlock = false;
if (directUnlock) { if (directUnlock) {
if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable')); if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable'));
if (!pending.supportsPrf) {
const shouldSaveLoginOnly = await confirmSaveLoginOnlyAccountPasskey();
if (!shouldSaveLoginOnly) {
onNotify('warning', t('txt_account_passkey_not_saved'));
return null;
}
savedWithoutDirectUnlock = true;
} else {
try { try {
keySet = await buildAccountPasskeyPrfKeySet(pending, { keySet = await buildAccountPasskeyPrfKeySet(pending, {
symEncKey: session.symEncKey, symEncKey: session.symEncKey,
@@ -254,6 +262,7 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
savedWithoutDirectUnlock = true; savedWithoutDirectUnlock = true;
} }
} }
}
const credential = await saveAccountPasskey(authedFetch, { const credential = await saveAccountPasskey(authedFetch, {
name: normalizedName, name: normalizedName,
token: pending.token, token: pending.token,
+35 -3
View File
@@ -150,6 +150,17 @@ function shouldRetryWithLegacyPrf(error: unknown): boolean {
return name === 'NotSupportedError' || name === 'SyntaxError' || name === 'TypeError'; return name === 'NotSupportedError' || name === 'SyntaxError' || name === 'TypeError';
} }
function shouldRetryCreateWithoutPrf(error: unknown): boolean {
const name = error instanceof DOMException || error instanceof Error ? error.name : '';
const message = error instanceof DOMException || error instanceof Error ? error.message : '';
return (
name === 'NotSupportedError' ||
name === 'SyntaxError' ||
name === 'TypeError' ||
(name === 'UnknownError' && /transient/i.test(message))
);
}
async function getPublicKeyCredentialWithPrf( async function getPublicKeyCredentialWithPrf(
options: PublicKeyCredentialRequestOptions, options: PublicKeyCredentialRequestOptions,
salt: Uint8Array, salt: Uint8Array,
@@ -265,17 +276,38 @@ export async function assertAccountPasskey(
} }
export async function createAccountPasskeyCredential( export async function createAccountPasskeyCredential(
response: { options: unknown; token: string } response: { options: unknown; token: string },
requestPrf: boolean = false
): Promise<PendingAccountPasskeyCredential> { ): Promise<PendingAccountPasskeyCredential> {
if (!window.PublicKeyCredential || !navigator.credentials) { if (!window.PublicKeyCredential || !navigator.credentials) {
throw new Error(t('txt_passkey_browser_not_supported')); throw new Error(t('txt_passkey_browser_not_supported'));
} }
const nativeOptions = cloneCreationOptions(response.options); const nativeOptions = cloneCreationOptions(response.options);
(nativeOptions as any).extensions = { const createWithOptions = async (options: PublicKeyCredentialCreationOptions): Promise<PublicKeyCredential> => {
const credential = await navigator.credentials.create({ publicKey: options });
if (!(credential instanceof PublicKeyCredential)) {
throw new Error(t('txt_no_passkey_created'));
}
return credential;
};
let credential: PublicKeyCredential;
if (requestPrf) {
const prfOptions: PublicKeyCredentialCreationOptions = {
...nativeOptions,
extensions: {
...((nativeOptions as any).extensions || {}), ...((nativeOptions as any).extensions || {}),
prf: {}, prf: {},
} as any,
}; };
const credential = await navigator.credentials.create({ publicKey: nativeOptions }); try {
credential = await createWithOptions(prfOptions);
} catch (error) {
if (!shouldRetryCreateWithoutPrf(error)) throw error;
credential = await createWithOptions(nativeOptions);
}
} else {
credential = await createWithOptions(nativeOptions);
}
if (!(credential instanceof PublicKeyCredential)) { if (!(credential instanceof PublicKeyCredential)) {
throw new Error(t('txt_no_passkey_created')); throw new Error(t('txt_no_passkey_created'));
} }