Compare commits

...

10 Commits

Author SHA1 Message Date
Alex Tran
03e8f98c2c feat: consider DAR when extracting video dimension 2026-01-16 02:47:57 +00:00
Jason Rasmussen
843d563178 refactor(web): admin page layout (#25281)
* refactor(web): admin page layout

* chore: remove unused props
2026-01-15 18:58:43 -05:00
Min Idzelis
256d62e22d feat: thumbhash improvments for reactive prop updates (#25287) 2026-01-15 18:57:43 -05:00
shenlong
91592aa48e fix(mobile): drop unique constraint on cloud_id (#25291)
fix: drop unique constraint on cloud_id

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 17:06:29 -06:00
shenlong
2ac113624b chore: remote unused sync_cloud_ids key (#25290)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 16:56:05 -06:00
renovate[bot]
0052979853 chore(deps): update dependency svelte to v5.46.4 [security] (#25284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 22:10:17 +01:00
renovate[bot]
79b6c4ac70 chore(deps): update dependency @sveltejs/kit to v2.49.5 [security] (#25280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 15:07:26 -05:00
Alex
95eb3e26c3 chore: sidebar spacing (#25278) 2026-01-15 10:35:01 -06:00
Alex
613dc858cb chore: tweak table text size (#25276) 2026-01-15 11:06:34 -05:00
shenlong
2f3fbd7dc5 fix: ignore duplicate cloud ID updates (#25271)
* fix: ignore duplicate remote updates

* update cloudId when any one of the ETag part is mismatched

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-01-15 09:15:56 -06:00
28 changed files with 479 additions and 398 deletions

View File

@@ -2124,7 +2124,6 @@
"sync": "Sync",
"sync_albums": "Sync albums",
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
"sync_cloud_ids": "Sync Cloud IDs",
"sync_local": "Sync Local",
"sync_remote": "Sync Remote",
"sync_status": "Sync Status",

File diff suppressed because one or more lines are too long

View File

@@ -21,6 +21,7 @@ Future<void> syncCloudIds(ProviderContainer ref) async {
if (!CurrentPlatform.isIOS) {
return;
}
final logger = Logger('migrateCloudIds');
final db = ref.read(driftProvider);
// Populate cloud IDs for local assets that don't have one yet
@@ -29,9 +30,7 @@ Future<void> syncCloudIds(ProviderContainer ref) async {
final serverInfo = await ref.read(serverInfoProvider.notifier).getServerInfo();
final canUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 4);
if (!canUpdateMetadata) {
Logger(
'migrateCloudIds',
).fine('Server version does not support asset metadata updates. Skipping cloudId migration.');
logger.fine('Server version does not support asset metadata updates. Skipping cloudId migration.');
return;
}
final canBulkUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 5);
@@ -40,25 +39,35 @@ Future<void> syncCloudIds(ProviderContainer ref) async {
try {
await ref.read(syncStreamServiceProvider).sync();
} catch (e, s) {
Logger('migrateCloudIds').fine('Failed to complete remote sync before cloudId migration.', e, s);
logger.fine('Failed to complete remote sync before cloudId migration.', e, s);
return;
}
// Fetch the mapping for backed up assets that have a cloud ID locally but do not have a cloud ID on the server
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
Logger('migrateCloudIds').warning('Current user is null. Aborting cloudId migration.');
logger.warning('Current user is null. Aborting cloudId migration.');
return;
}
final mappingsToUpdate = await _fetchCloudIdMappings(db, currentUser.id);
// Deduplicate mappings as a single remote asset ID can match multiple local assets
final seenRemoteAssetIds = <String>{};
final uniqueMapping = mappingsToUpdate.where((mapping) {
if (!seenRemoteAssetIds.add(mapping.remoteAssetId)) {
logger.fine('Duplicate remote asset ID found: ${mapping.remoteAssetId}. Skipping duplicate entry.');
return false;
}
return true;
}).toList();
final assetApi = ref.read(apiServiceProvider).assetsApi;
if (canBulkUpdateMetadata) {
await _bulkUpdateCloudIds(assetApi, mappingsToUpdate);
await _bulkUpdateCloudIds(assetApi, uniqueMapping);
return;
}
await _sequentialUpdateCloudIds(assetApi, mappingsToUpdate);
await _sequentialUpdateCloudIds(assetApi, uniqueMapping);
}
Future<void> _sequentialUpdateCloudIds(AssetsApi assetsApi, List<_CloudIdMapping> mappings) async {
@@ -152,10 +161,10 @@ Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId)
// Skip locked assets as we cannot update them without unlocking first
drift.remoteAssetEntity.visibility.isNotValue(AssetVisibility.locked.index) &
(drift.remoteAssetCloudIdEntity.cloudId.isNull() |
((drift.remoteAssetCloudIdEntity.adjustmentTime.isNotExp(drift.localAssetEntity.adjustmentTime)) &
(drift.remoteAssetCloudIdEntity.latitude.isNotExp(drift.localAssetEntity.latitude)) &
(drift.remoteAssetCloudIdEntity.longitude.isNotExp(drift.localAssetEntity.longitude)) &
(drift.remoteAssetCloudIdEntity.createdAt.isNotExp(drift.localAssetEntity.createdAt)))),
drift.remoteAssetCloudIdEntity.adjustmentTime.isNotExp(drift.localAssetEntity.adjustmentTime) |
drift.remoteAssetCloudIdEntity.latitude.isNotExp(drift.localAssetEntity.latitude) |
drift.remoteAssetCloudIdEntity.longitude.isNotExp(drift.localAssetEntity.longitude) |
drift.remoteAssetCloudIdEntity.createdAt.isNotExp(drift.localAssetEntity.createdAt)),
);
return query.map((row) {
return (

View File

@@ -5,7 +5,7 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class RemoteAssetCloudIdEntity extends Table with DriftDefaultsMixin {
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get cloudId => text().unique().nullable()();
TextColumn get cloudId => text().nullable()();
DateTimeColumn get createdAt => dateTime().nullable()();

View File

@@ -438,7 +438,6 @@ class $RemoteAssetCloudIdEntityTable extends i2.RemoteAssetCloudIdEntity
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways('UNIQUE'),
);
static const i0.VerificationMeta _createdAtMeta = const i0.VerificationMeta(
'createdAt',

View File

@@ -6903,7 +6903,6 @@ i1.GeneratedColumn<String> _column_99(String aliasedName) =>
aliasedName,
true,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'),
);
i1.GeneratedColumn<DateTime> _column_100(String aliasedName) =>
i1.GeneratedColumn<DateTime>(

View File

@@ -5391,7 +5391,6 @@ class RemoteAssetCloudIdEntity extends Table
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'),
);
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at',

506
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -258,6 +258,7 @@ export class MediaRepository {
colorPrimaries: stream.color_primaries,
colorSpace: stream.color_space,
colorTransfer: stream.color_transfer,
displayAspectRatio: stream.display_aspect_ratio,
})),
audioStreams: results.streams
.filter((stream) => stream.codec_type === 'audio')

View File

@@ -599,6 +599,21 @@ describe(MetadataService.name, () => {
);
});
it('should apply Display Aspect Ratio (DAR) for anamorphic video', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
mocks.media.probe.mockResolvedValue(probeStub.videoStreamAnamorphic);
mockReadTags({});
await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
// Anamorphic video: 1440x1080 with DAR 16:9 should display as 1920x1080
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
expect.objectContaining({ exifImageWidth: 1920, exifImageHeight: 1080 }),
{ lockedPropertiesBehavior: 'skip' },
);
});
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
...assetStub.livePhotoWithOriginalFileName,

View File

@@ -989,12 +989,15 @@ export class MetadataService extends BaseService {
const tags: Pick<ImmichTags, 'Duration' | 'Orientation' | 'ImageWidth' | 'ImageHeight'> = {};
if (videoStreams[0]) {
// Set video dimensions
if (videoStreams[0].width) {
tags.ImageWidth = videoStreams[0].width;
// Set video dimensions, considering Display Aspect Ratio (DAR) for anamorphic videos
const { width, height, displayAspectRatio } = videoStreams[0];
const displayDimensions = this.applyDisplayAspectRatio(width, height, displayAspectRatio);
if (displayDimensions.width) {
tags.ImageWidth = displayDimensions.width;
}
if (videoStreams[0].height) {
tags.ImageHeight = videoStreams[0].height;
if (displayDimensions.height) {
tags.ImageHeight = displayDimensions.height;
}
switch (videoStreams[0].rotation) {
@@ -1023,4 +1026,44 @@ export class MetadataService extends BaseService {
return tags;
}
/**
* Calculates the display dimensions of a video based on its Display Aspect Ratio (DAR).
* DAR accounts for anamorphic videos where the stored pixel dimensions differ from the intended display dimensions.
* For example, a 1440x1080 video with DAR 16:9 should display as 1920x1080 (1440 * 16/9 / (1440/1080) = 1920).
*/
private applyDisplayAspectRatio(
width: number | undefined,
height: number | undefined,
displayAspectRatio: string | undefined,
): { width?: number; height?: number } {
if (!width || !height || !displayAspectRatio) {
return { width, height };
}
// Parse DAR string (e.g., "16:9" or "4:3")
const darMatch = displayAspectRatio.match(/^(\d+):(\d+)$/);
if (!darMatch) {
return { width, height };
}
const darWidth = Number.parseInt(darMatch[1], 10);
const darHeight = Number.parseInt(darMatch[2], 10);
if (!darWidth || !darHeight) {
return { width, height };
}
const dar = darWidth / darHeight;
const storedAspectRatio = width / height;
// If DAR is effectively the same as stored aspect ratio (within a small tolerance), no adjustment needed
if (Math.abs(dar - storedAspectRatio) < 0.01) {
return { width, height };
}
// Apply DAR by adjusting width while keeping height constant
// This matches how video players typically handle anamorphic content
const displayWidth = Math.round(height * dar);
return { width: displayWidth, height };
}
}

View File

@@ -85,6 +85,7 @@ export interface VideoStreamInfo {
colorPrimaries?: string;
colorSpace?: string;
colorTransfer?: string;
displayAspectRatio?: string;
}
export interface AudioStreamInfo {

View File

@@ -272,4 +272,21 @@ export const probeStub = {
},
],
}),
videoStreamAnamorphic: Object.freeze<VideoInfo>({
...probeStubDefault,
videoStreams: [
{
index: 0,
height: 1080,
width: 1440,
codecName: 'h264',
frameCount: 100,
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
displayAspectRatio: '16:9',
},
],
}),
};

View File

@@ -97,7 +97,7 @@
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.46.1",
"svelte": "5.46.4",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.1.7",

View File

@@ -3,17 +3,27 @@ import { thumbHashToRGBA } from 'thumbhash';
/**
* Renders a thumbnail onto a canvas from a base64 encoded hash.
* @param canvas
* @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString)
*/
export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
const ctx = canvas.getContext('2d');
if (ctx) {
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash));
const pixels = ctx.createImageData(w, h);
canvas.width = w;
canvas.height = h;
pixels.data.set(rgba);
ctx.putImageData(pixels, 0, 0);
}
export function thumbhash(canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) {
render(canvas, options);
return {
update(newOptions: { base64ThumbHash: string }) {
render(canvas, newOptions);
},
};
}
const render = (canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) => {
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(options.base64ThumbHash));
const pixels = ctx.createImageData(w, h);
canvas.width = w;
canvas.height = h;
pixels.data.set(rgba);
ctx.putImageData(pixels, 0, 0);
};

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { AppRoute } from '$lib/constants';
import { NavbarItem } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiTrayFull } from '@mdi/js';
import { t } from 'svelte-i18n';
</script>
<div class="h-full flex flex-col justify-between gap-2">
<div class="flex flex-col pt-8 pe-4 gap-1">
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARIES} icon={mdiBookshelf} />
<NavbarItem title={$t('admin.queues')} href={AppRoute.ADMIN_QUEUES} icon={mdiTrayFull} />
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
</div>
<div class="mb-2 me-4">
<BottomInfo />
</div>
</div>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import type { HeaderButtonActionItem } from '$lib/types';
import {
Breadcrumbs,
Button,
Container,
ContextMenuButton,
HStack,
MenuItemType,
Scrollable,
isMenuItemType,
type BreadcrumbItem,
} from '@immich/ui';
import { mdiSlashForward } from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
type Props = {
breadcrumbs?: BreadcrumbItem[];
actions?: Array<HeaderButtonActionItem | MenuItemType>;
children?: Snippet;
};
let { breadcrumbs = [], actions = [], children }: Props = $props();
const enabledActions = $derived(
actions
.filter((action): action is HeaderButtonActionItem => !isMenuItemType(action))
.filter((action) => action.$if?.() ?? true),
);
</script>
<div class="h-full flex flex-col">
<div class="flex h-16 w-full justify-between items-center border-b py-2 px-4 md:px-2">
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
{#if enabledActions.length > 0}
<div class="hidden md:block">
<HStack gap={0}>
{#each enabledActions as action, i (i)}
<Button
variant="ghost"
size="small"
color={action.color ?? 'secondary'}
leadingIcon={action.icon}
onclick={() => action.onAction(action)}
title={action.data?.title}
>
{action.title}
</Button>
{/each}
</HStack>
</div>
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
{/if}
</div>
<Scrollable class="grow">
<Container class="p-2 pb-16" {children} />
</Scrollable>
</div>

View File

@@ -1,23 +1,12 @@
<script lang="ts">
import AdminSidebar from '$lib/components/AdminSidebar.svelte';
import PageContent from '$lib/components/layouts/PageContent.svelte';
import BreadcrumbActionPage from '$lib/components/BreadcrumbActionPage.svelte';
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { AppRoute } from '$lib/constants';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import type { HeaderButtonActionItem } from '$lib/types';
import {
AppShell,
AppShellHeader,
AppShellSidebar,
Breadcrumbs,
Button,
ContextMenuButton,
HStack,
MenuItemType,
Scrollable,
isMenuItemType,
type BreadcrumbItem,
} from '@immich/ui';
import { mdiSlashForward } from '@mdi/js';
import { AppShell, AppShellHeader, AppShellSidebar, MenuItemType, NavbarItem, type BreadcrumbItem } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiTrayFull } from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
@@ -27,52 +16,31 @@
children?: Snippet;
};
let { breadcrumbs, actions = [], children }: Props = $props();
const enabledActions = $derived(
actions
.filter((action): action is HeaderButtonActionItem => !isMenuItemType(action))
.filter((action) => action.$if?.() ?? true),
);
let { breadcrumbs, actions, children }: Props = $props();
</script>
<AppShell>
<AppShellHeader>
<NavigationBar showUploadButton={false} noBorder />
<NavigationBar noBorder />
</AppShellHeader>
<AppShellSidebar bind:open={sidebarStore.isOpen} class="border-none shadow-none">
<AdminSidebar />
<AppShellSidebar
bind:open={sidebarStore.isOpen}
class="border-none shadow-none h-full flex flex-col justify-between gap-2"
>
<div class="flex flex-col pt-8 pe-4 gap-1">
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARIES} icon={mdiBookshelf} />
<NavbarItem title={$t('admin.queues')} href={AppRoute.ADMIN_QUEUES} icon={mdiTrayFull} />
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
</div>
<div class="mb-2 me-4">
<BottomInfo />
</div>
</AppShellSidebar>
<div class="h-full flex flex-col">
<div class="flex h-16 w-full justify-between items-center border-b py-2 px-4 md:px-2">
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
{#if enabledActions.length > 0}
<div class="hidden md:block">
<HStack gap={0}>
{#each enabledActions as action, i (i)}
<Button
variant="ghost"
size="small"
color={action.color ?? 'secondary'}
leadingIcon={action.icon}
onclick={() => action.onAction(action)}
title={action.data?.title}
>
{action.title}
</Button>
{/each}
</HStack>
</div>
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
{/if}
</div>
<Scrollable class="grow">
<PageContent>
{@render children?.()}
</PageContent>
</Scrollable>
</div>
<BreadcrumbActionPage {breadcrumbs} {actions}>
{@render children?.()}
</BreadcrumbActionPage>
</AppShell>

View File

@@ -1,12 +0,0 @@
<script lang="ts">
import { Container } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
children?: Snippet;
};
const { children }: Props = $props();
</script>
<Container class="p-2 pb-16" {children} />

View File

@@ -14,13 +14,11 @@
interface Props {
hideNavbar?: boolean;
showUploadButton?: boolean;
title?: string | undefined;
description?: string | undefined;
scrollbar?: boolean;
use?: ActionArray;
actions?: Array<HeaderButtonActionItem | MenuItemType>;
header?: Snippet;
sidebar?: Snippet;
buttons?: Snippet;
children?: Snippet;
@@ -28,13 +26,11 @@
let {
hideNavbar = false,
showUploadButton = false,
title = undefined,
description = undefined,
scrollbar = true,
use = [],
actions = [],
header,
sidebar,
buttons,
children,
@@ -52,10 +48,8 @@
<header>
{#if !hideNavbar}
<NavigationBar {showUploadButton} onUploadClick={() => openFileUploadDialog()} />
<NavigationBar onUploadClick={() => openFileUploadDialog()} />
{/if}
{@render header?.()}
</header>
<div
tabindex="-1"

View File

@@ -25,14 +25,13 @@
import UserAvatar from '../user-avatar.svelte';
import AccountInfoPanel from './account-info-panel.svelte';
interface Props {
showUploadButton?: boolean;
type Props = {
onUploadClick?: () => void;
// TODO: remove once this is only used in <AppShellHeader>
noBorder?: boolean;
}
};
let { showUploadButton = true, onUploadClick, noBorder = false }: Props = $props();
let { onUploadClick, noBorder = false }: Props = $props();
let shouldShowAccountInfoPanel = $state(false);
let shouldShowNotificationPanel = $state(false);
@@ -105,7 +104,7 @@
/>
{/if}
{#if !page.url.pathname.includes('/admin') && showUploadButton && onUploadClick}
{#if !page.url.pathname.includes('/admin') && onUploadClick}
<Button
leadingIcon={mdiTrayArrowUp}
onclick={onUploadClick}

View File

@@ -87,7 +87,7 @@
bind:isSelected={isSharingSelected}
></SideBarLink>
<p class="text-xs p-6 dark:text-immich-dark-fg uppercase">{$t('library')}</p>
<p class="text-xs py-5 ps-6 dark:text-immich-dark-fg uppercase">{$t('library')}</p>
<SideBarLink
title={$t('favorites')}

View File

@@ -166,7 +166,7 @@
</div>
</SettingAccordion>
<div class="flex justify-end">
<div class="flex justify-end mt-4">
<Button shape="round" type="submit" size="small" onclick={() => handleSave()}>{$t('save')}</Button>
</div>
</div>

View File

@@ -41,7 +41,7 @@
</div>
{#if keys.length > 0}
<Table class="mt-4" striped spacing="small">
<Table class="mt-4" striped spacing="small" size="small">
<TableHeader>
<TableHeading>{$t('name')}</TableHeading>
<TableHeading>{$t('permission')}</TableHeading>

View File

@@ -67,7 +67,7 @@
<section class="my-6 w-full">
<Heading size="tiny">{$t('photos_and_videos')}</Heading>
<Table striped spacing="medium" class="mt-4">
<Table striped spacing="small" class="mt-4" size="small">
<TableHeader>
<TableHeading class="w-1/4">{$t('view_name')}</TableHeading>
<TableHeading class="w-1/4">{$t('photos')}</TableHeading>
@@ -83,7 +83,7 @@
</Table>
<Heading size="tiny" class="mt-8">{$t('albums')}</Heading>
<Table striped spacing="medium" class="mt-4">
<Table striped spacing="small" class="mt-4" size="small">
<TableHeader>
<TableHeading class="w-1/2">{$t('owned')}</TableHeading>
<TableHeading class="w-1/2">{$t('shared')}</TableHeading>

View File

@@ -102,7 +102,7 @@
);
</script>
<UserPageLayout hideNavbar={assetInteraction.selectionActive} showUploadButton scrollbar={false}>
<UserPageLayout hideNavbar={assetInteraction.selectionActive} scrollbar={false}>
<Timeline
enableRouting={true}
bind:timelineManager

View File

@@ -76,7 +76,7 @@
<Container size="large" center class="my-4">
<div class="flex flex-col items-center gap-2" in:fade={{ duration: 500 }}>
{#if libraries.length > 0}
<Table striped>
<Table striped size="small" spacing="small">
<TableHeader>
<TableHeading class={classes.column1}>{$t('name')}</TableHeading>
<TableHeading class={classes.column2}>{$t('owner')}</TableHeading>

View File

@@ -66,7 +66,7 @@
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[Create]}>
<Container center size="large">
<Table class="mt-4" striped spacing="large">
<Table class="mt-4" striped spacing="small" size="small">
<TableHeader>
<TableHeading class={classes.column1}>{$t('email')}</TableHeading>
<TableHeading class={classes.column2}>{$t('name')}</TableHeading>