chore: i18n pass, update progress bar

This commit is contained in:
izzy
2025-11-25 11:07:11 +00:00
parent 1cdffeb3be
commit 390f0b2817
5 changed files with 113 additions and 61 deletions

View File

@@ -18,6 +18,7 @@
IconButton,
menuManager,
modalManager,
ProgressBar,
Stack,
Text,
type ContextMenuBaseProps,
@@ -36,15 +37,15 @@
function mapBackups(filenames: string[]) {
return filenames.map((filename) => {
const date = /(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/.exec(filename);
const hoursAgo = date
const minutesAgo = date
? Math.floor(
(+Date.now() - +new Date(`${date[1]}-${date[2]}-${date[3]}T${date[4]}:${date[5]}:${date[6]}`)) / 36e5,
(+Date.now() - +new Date(`${date[1]}-${date[2]}-${date[3]}T${date[4]}:${date[5]}:${date[6]}`)) / 60_000,
)
: null;
return {
filename,
hoursAgo,
minutesAgo,
};
});
}
@@ -61,9 +62,9 @@
async function restore(filename: string) {
const confirm = await modalManager.showDialog({
confirmText: 'Restore',
title: 'Restore Backup',
prompt: 'Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.',
confirmText: $t('restore'),
title: $t('admin.maintenance_restore_backup'),
prompt: $t('admin.maintenance_restore_backup_description'),
});
if (confirm) {
@@ -82,9 +83,9 @@
async function remove(filename: string) {
const confirm = await modalManager.showDialog({
confirmText: 'Delete',
title: 'Delete Backup',
prompt: 'This file will be irrevocably deleted.',
confirmText: $t('delete'),
title: $t('admin.maintenance_delete_backup'),
prompt: $t('admin.maintenance_delete_backup_description'),
});
if (confirm) {
@@ -97,7 +98,7 @@
backups = backups.filter((backup) => backup.filename !== filename);
} catch (error) {
handleError(error, 'failed to delete backup i18n');
handleError(error, $t('admin.maintenance_delete_error'));
} finally {
deleting.delete(filename);
}
@@ -114,14 +115,14 @@
target: event.currentTarget as HTMLElement,
items: [
{
title: 'Download',
title: $t('download'),
icon: mdiDownload,
onSelect() {
void download(filename);
},
},
{
title: 'Delete',
title: $t('delete'),
icon: mdiTrashCanOutline,
color: 'danger',
onSelect() {
@@ -153,7 +154,7 @@
const { backups: newList } = await listBackups();
backups = mapBackups(newList);
} catch (error) {
handleError(error, 'Could not upload backup, is it an .sql/.sql.gz file?');
handleError(error, $t('admin.maintenance_upload_backup_error'));
} finally {
uploadProgress = -1;
}
@@ -165,15 +166,13 @@
<CardBody>
{#if uploadProgress === -1}
<HStack>
<Text class="grow">Upload database backup file</Text>
<Button size="small" onclick={upload}>Select file</Button>
<Text class="grow">{$t('admin.maintenance_upload_backup')}</Text>
<Button size="tiny" onclick={upload}>{$t('select_from_computer')}</Button>
</HStack>
{:else}
<HStack>
<Text class="grow">Uploading...</Text>
<div class="grow h-2.5 bg-gray-300 rounded-full overflow-hidden">
<div class="h-full bg-blue-600 transition-all duration-700" style="width: {uploadProgress * 100}%"></div>
</div>
<HStack gap={8}>
<Text class="grow">{$t('asset_uploading')}</Text>
<ProgressBar progress={uploadProgress} size="tiny" />
</HStack>
{/if}
</CardBody>
@@ -185,20 +184,48 @@
<HStack>
<Stack class="grow">
<Text>{backup.filename}</Text>
{#if typeof backup.hoursAgo === 'number'}
{#if backup.hoursAgo <= 24}
{#if typeof backup.minutesAgo === 'number'}
<Text color="info" size="small">
{#if backup.minutesAgo <= 1}
{$t('created_minute_ago')}
{:else if backup.minutesAgo < 60}
{$t('created_minutes_ago', {
values: {
count: backup.minutesAgo,
},
})}
{:else if backup.minutesAgo < 60 * 2}
{$t('created_hour_ago')}
{:else if backup.minutesAgo < 60 * 24}
{$t('created_hours_ago', {
values: {
count: Math.floor(backup.minutesAgo / 60),
},
})}
{:else if backup.minutesAgo < 60 * 24 * 2}
{$t('created_day_ago')}
{:else}
{$t('created_days_ago', {
values: {
count: Math.floor(backup.minutesAgo / (60 * 24)),
},
})}
{/if}
</Text>
<!-- {#if backup.hoursAgo <= 24}
<Text color="info" size="small">Created {backup.hoursAgo} hours ago</Text>
{:else if backup.hoursAgo <= 48}
<Text color="info" size="small">Created 1 day ago</Text>
{:else}
<Text color="info" size="small">Created {Math.floor(backup.hoursAgo / 24)} days ago</Text>
{/if}
{/if} -->
{/if}
</Stack>
<Button size="small" disabled={deleting.has(backup.filename)} onclick={() => restore(backup.filename)}
>Restore</Button
>{$t('restore')}</Button
>
<IconButton
shape="round"
variant="ghost"
@@ -207,7 +234,7 @@
class="shrink-0"
disabled={deleting.has(backup.filename)}
onclick={(event: Event) => handleOpen(event, { position: 'top-right' }, backup.filename)}
aria-label="Open menu"
aria-label={$t('open')}
/>
</HStack>
</CardBody>

View File

@@ -4,6 +4,7 @@
import { Button, Card, CardBody, Heading, HStack, Icon, Scrollable, Stack, Text } from '@immich/ui';
import { mdiAlert, mdiArrowLeft, mdiArrowRight, mdiCheck, mdiClose, mdiRefresh } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
end?: () => void;
@@ -19,22 +20,11 @@
}
onMount(reload);
const i18nMap = {
'encoded-video': 'Encoded Video',
library: 'Library',
upload: 'Upload',
profile: 'Profile',
thumbs: 'Thumbs',
backups: 'Backups',
};
</script>
{#if stage === 0}
<Heading size="large" color="primary" tag="h1">Restore Your Library</Heading>
<Text
>Before restoring a database backup, you must ensure your library has been restored or is otherwise already present.</Text
>
<Heading size="large" color="primary" tag="h1">{$t('maintenance_restore_library')}</Heading>
<Text>{$t('maintenance_restore_library_description')}</Text>
<Card>
<CardBody>
<Stack>
@@ -46,12 +36,10 @@
color={`rgb(var(--immich-ui-${writable ? 'success' : 'danger'}))`}
/>
<Text
>{i18nMap[folder as keyof typeof i18nMap]} ({writable
? 'readable and writable'
: readable
? 'not writable'
: 'not readable'})</Text
>
>{folder} ({$t(
`maintenance_restore_library_folder_${writable ? 'pass' : readable ? 'write_fail' : 'read_fail'}`,
)})
</Text>
</HStack>
{/each}
{#each integrity.storage as { folder, files } (folder)}
@@ -65,47 +53,57 @@
<Stack gap={0} class="items-start">
<Text>
{#if files}
{i18nMap[folder as keyof typeof i18nMap]} has {files} folder(s)
{$t('maintenance_restore_library_folder_has_files', {
values: {
folder,
count: files,
},
})}
{:else}
{i18nMap[folder as keyof typeof i18nMap]} is missing files!
{$t('maintenance_restore_library_folder_no_files', {
values: {
folder,
},
})}
{/if}
</Text>
{#if !files && (folder === 'profile' || folder === 'upload')}
<Text variant="italic">You may be missing files</Text>
<Text variant="italic">{$t('maintenance_restore_library_hint_missing_files')}</Text>
{/if}
{#if !files && (folder === 'encoded-video' || folder === 'thumbs')}
<Text variant="italic">You can regenerate these later in settings</Text>
<Text variant="italic">{$t('maintenance_restore_library_hint_regenerate_later')}</Text>
{/if}
{#if !files && folder === 'library'}
<Text variant="italic">Using storage template? You may be missing files</Text>
<Text variant="italic">{$t('maintenance_restore_library_hint_storage_template_missing_files')}</Text
>
{/if}
</Stack>
</HStack>
{/if}
{/each}
<Button leadingIcon={mdiRefresh} variant="ghost" onclick={reload}>Refresh</Button>
<Button leadingIcon={mdiRefresh} variant="ghost" onclick={reload}>{$t('refresh')}</Button>
{:else}
<HStack>
<Icon icon={mdiRefresh} color="rgb(var(--immich-ui-primary))" />
<Text>Loading integrity checks and heuristics...</Text>
<Text>{$t('maintenance_restore_library_loading')}</Text>
</HStack>
{/if}
</Stack>
</CardBody>
</Card>
<Text>If this looks correct, continue to restoring a backup!</Text>
<Text>{$t('maintenance_restore_library_confirm')}</Text>
<HStack>
<Button onclick={props.end} variant="ghost">Cancel</Button>
<Button onclick={() => stage++} trailingIcon={mdiArrowRight}>Next</Button>
<Button onclick={props.end} variant="ghost">{$t('cancel')}</Button>
<Button onclick={() => stage++} trailingIcon={mdiArrowRight}>{$t('next')}</Button>
</HStack>
{:else}
<Heading size="large" color="primary" tag="h1">Restore From Backup</Heading>
<Heading size="large" color="primary" tag="h1">{$t('maintenance_restore_from_backup')}</Heading>
<Scrollable class="max-h-80">
<MaintenanceBackupsList />
</Scrollable>
<HStack>
<Button onclick={props.end} variant="ghost">Cancel</Button>
<Button onclick={() => stage--} variant="ghost" leadingIcon={mdiArrowLeft}>Back</Button>
<Button onclick={props.end} variant="ghost">{$t('cancel')}</Button>
<Button onclick={() => stage--} variant="ghost" leadingIcon={mdiArrowLeft}>{$t('back')}</Button>
</HStack>
{/if}