1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

Merge branch 'main' of github.com:immich-app/immich

This commit is contained in:
Alex Tran 2023-02-27 18:29:02 -06:00
commit 8abe6909ca
No known key found for this signature in database
GPG key ID: E4954BC787B85C8A
19 changed files with 522 additions and 139 deletions

View file

@ -3,14 +3,15 @@ import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
FavoriteSelectionNotifier(this.ref) : super({}) {
state = ref.watch(assetProvider).allAssets
FavoriteSelectionNotifier(this.assetsState, this.assetNotifier) : super({}) {
state = assetsState.allAssets
.where((asset) => asset.isFavorite)
.map((asset) => asset.id)
.toSet();
}
final Ref ref;
final AssetsState assetsState;
final AssetNotifier assetNotifier;
void _setFavoriteForAssetId(String id, bool favorite) {
if (!favorite) {
@ -29,7 +30,7 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
_setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
await ref.watch(assetProvider.notifier).toggleFavorite(
await assetNotifier.toggleFavorite(
asset,
state.contains(asset.id),
);
@ -37,8 +38,8 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
Future<void> addToFavorites(Iterable<Asset> assets) {
state = state.union(assets.map((a) => a.id).toSet());
final futures = assets.map((a) =>
ref.watch(assetProvider.notifier).toggleFavorite(
final futures = assets.map((a) =>
assetNotifier.toggleFavorite(
a,
true,
),
@ -50,7 +51,10 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
final favoriteProvider =
StateNotifierProvider<FavoriteSelectionNotifier, Set<String>>((ref) {
return FavoriteSelectionNotifier(ref);
return FavoriteSelectionNotifier(
ref.watch(assetProvider),
ref.watch(assetProvider.notifier),
);
});
final favoriteAssetProvider = StateProvider((ref) {

View file

@ -740,6 +740,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
mockito:
dependency: "direct dev"
description:
name: mockito
sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe"
url: "https://pub.dev"
source: hosted
version: "5.3.2"
nested:
dependency: transitive
description:

View file

@ -64,6 +64,7 @@ dev_dependencies:
flutter_launcher_icons: "^0.9.2"
flutter_native_splash: ^2.2.16
isar_generator: *isar_version
mockito: ^5.3.2
integration_test:
sdk: flutter

View file

@ -0,0 +1,104 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateNiceMocks([
MockSpec<AssetsState>(),
MockSpec<AssetNotifier>(),
])
import 'favorite_provider_test.mocks.dart';
Asset _getTestAsset(String id, bool favorite) {
return Asset(
remoteId: id,
deviceAssetId: '',
deviceId: '',
ownerId: '',
fileCreatedAt: DateTime.now(),
fileModifiedAt: DateTime.now(),
durationInSeconds: 0,
fileName: '',
isFavorite: favorite,
);
}
void main() {
group("Test favoriteProvider", () {
late MockAssetsState assetsState;
late MockAssetNotifier assetNotifier;
late ProviderContainer container;
late StateNotifierProvider<FavoriteSelectionNotifier, Set<String>> testFavoritesProvider;
setUp(() {
assetsState = MockAssetsState();
assetNotifier = MockAssetNotifier();
container = ProviderContainer();
testFavoritesProvider =
StateNotifierProvider<FavoriteSelectionNotifier, Set<String>>((ref) {
return FavoriteSelectionNotifier(
assetsState,
assetNotifier,
);
});
},);
test("Empty favorites provider", () {
when(assetsState.allAssets).thenReturn([]);
expect(<String>{}, container.read(testFavoritesProvider));
});
test("Non-empty favorites provider", () {
when(assetsState.allAssets).thenReturn([
_getTestAsset("001", false),
_getTestAsset("002", true),
_getTestAsset("003", false),
_getTestAsset("004", false),
_getTestAsset("005", true),
]);
expect(<String>{"002", "005"}, container.read(testFavoritesProvider));
});
test("Toggle favorite", () {
when(assetNotifier.toggleFavorite(null, false))
.thenAnswer((_) async => false);
final testAsset1 = _getTestAsset("001", false);
final testAsset2 = _getTestAsset("002", true);
when(assetsState.allAssets).thenReturn([testAsset1, testAsset2]);
expect(<String>{"002"}, container.read(testFavoritesProvider));
container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset2);
expect(<String>{}, container.read(testFavoritesProvider));
container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset1);
expect(<String>{"001"}, container.read(testFavoritesProvider));
});
test("Add favorites", () {
when(assetNotifier.toggleFavorite(null, false))
.thenAnswer((_) async => false);
when(assetsState.allAssets).thenReturn([]);
expect(<String>{}, container.read(testFavoritesProvider));
container.read(testFavoritesProvider.notifier).addToFavorites(
[
_getTestAsset("001", false),
_getTestAsset("002", false),
],
);
expect(<String>{"001", "002"}, container.read(testFavoritesProvider));
});
});
}

View file

@ -0,0 +1,259 @@
// Mocks generated by Mockito 5.3.2 from annotations
// in immich_mobile/test/favorite_provider_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i5;
import 'package:hooks_riverpod/hooks_riverpod.dart' as _i7;
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'
as _i6;
import 'package:immich_mobile/shared/models/asset.dart' as _i4;
import 'package:immich_mobile/shared/providers/asset.provider.dart' as _i2;
import 'package:logging/logging.dart' as _i3;
import 'package:mockito/mockito.dart' as _i1;
import 'package:state_notifier/state_notifier.dart' as _i8;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
class _FakeAssetsState_0 extends _i1.SmartFake implements _i2.AssetsState {
_FakeAssetsState_0(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
class _FakeLogger_1 extends _i1.SmartFake implements _i3.Logger {
_FakeLogger_1(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
/// A class which mocks [AssetsState].
///
/// See the documentation for Mockito's code generation for more information.
class MockAssetsState extends _i1.Mock implements _i2.AssetsState {
@override
List<_i4.Asset> get allAssets => (super.noSuchMethod(
Invocation.getter(#allAssets),
returnValue: <_i4.Asset>[],
returnValueForMissingStub: <_i4.Asset>[],
) as List<_i4.Asset>);
@override
_i5.Future<_i2.AssetsState> withRenderDataStructure(
_i6.AssetGridLayoutParameters? layout) =>
(super.noSuchMethod(
Invocation.method(
#withRenderDataStructure,
[layout],
),
returnValue: _i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0(
this,
Invocation.method(
#withRenderDataStructure,
[layout],
),
)),
returnValueForMissingStub:
_i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0(
this,
Invocation.method(
#withRenderDataStructure,
[layout],
),
)),
) as _i5.Future<_i2.AssetsState>);
@override
_i2.AssetsState withAdditionalAssets(List<_i4.Asset>? toAdd) =>
(super.noSuchMethod(
Invocation.method(
#withAdditionalAssets,
[toAdd],
),
returnValue: _FakeAssetsState_0(
this,
Invocation.method(
#withAdditionalAssets,
[toAdd],
),
),
returnValueForMissingStub: _FakeAssetsState_0(
this,
Invocation.method(
#withAdditionalAssets,
[toAdd],
),
),
) as _i2.AssetsState);
}
/// A class which mocks [AssetNotifier].
///
/// See the documentation for Mockito's code generation for more information.
class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier {
@override
_i3.Logger get log => (super.noSuchMethod(
Invocation.getter(#log),
returnValue: _FakeLogger_1(
this,
Invocation.getter(#log),
),
returnValueForMissingStub: _FakeLogger_1(
this,
Invocation.getter(#log),
),
) as _i3.Logger);
@override
set onError(_i7.ErrorListener? _onError) => super.noSuchMethod(
Invocation.setter(
#onError,
_onError,
),
returnValueForMissingStub: null,
);
@override
bool get mounted => (super.noSuchMethod(
Invocation.getter(#mounted),
returnValue: false,
returnValueForMissingStub: false,
) as bool);
@override
_i5.Stream<_i2.AssetsState> get stream => (super.noSuchMethod(
Invocation.getter(#stream),
returnValue: _i5.Stream<_i2.AssetsState>.empty(),
returnValueForMissingStub: _i5.Stream<_i2.AssetsState>.empty(),
) as _i5.Stream<_i2.AssetsState>);
@override
_i2.AssetsState get state => (super.noSuchMethod(
Invocation.getter(#state),
returnValue: _FakeAssetsState_0(
this,
Invocation.getter(#state),
),
returnValueForMissingStub: _FakeAssetsState_0(
this,
Invocation.getter(#state),
),
) as _i2.AssetsState);
@override
set state(_i2.AssetsState? value) => super.noSuchMethod(
Invocation.setter(
#state,
value,
),
returnValueForMissingStub: null,
);
@override
_i2.AssetsState get debugState => (super.noSuchMethod(
Invocation.getter(#debugState),
returnValue: _FakeAssetsState_0(
this,
Invocation.getter(#debugState),
),
returnValueForMissingStub: _FakeAssetsState_0(
this,
Invocation.getter(#debugState),
),
) as _i2.AssetsState);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,
returnValueForMissingStub: false,
) as bool);
@override
_i5.Future<void> rebuildAssetGridDataStructure() => (super.noSuchMethod(
Invocation.method(
#rebuildAssetGridDataStructure,
[],
),
returnValue: _i5.Future<void>.value(),
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override
void onNewAssetUploaded(_i4.Asset? newAsset) => super.noSuchMethod(
Invocation.method(
#onNewAssetUploaded,
[newAsset],
),
returnValueForMissingStub: null,
);
@override
dynamic deleteAssets(Set<_i4.Asset>? deleteAssets) => super.noSuchMethod(
Invocation.method(
#deleteAssets,
[deleteAssets],
),
returnValueForMissingStub: null,
);
@override
_i5.Future<bool> toggleFavorite(
_i4.Asset? asset,
bool? status,
) =>
(super.noSuchMethod(
Invocation.method(
#toggleFavorite,
[
asset,
status,
],
),
returnValue: _i5.Future<bool>.value(false),
returnValueForMissingStub: _i5.Future<bool>.value(false),
) as _i5.Future<bool>);
@override
bool updateShouldNotify(
_i2.AssetsState? old,
_i2.AssetsState? current,
) =>
(super.noSuchMethod(
Invocation.method(
#updateShouldNotify,
[
old,
current,
],
),
returnValue: false,
returnValueForMissingStub: false,
) as bool);
@override
_i7.RemoveListener addListener(
_i8.Listener<_i2.AssetsState>? listener, {
bool? fireImmediately = true,
}) =>
(super.noSuchMethod(
Invocation.method(
#addListener,
[listener],
{#fireImmediately: fireImmediately},
),
returnValue: () {},
returnValueForMissingStub: () {},
) as _i7.RemoveListener);
@override
void dispose() => super.noSuchMethod(
Invocation.method(
#dispose,
[],
),
returnValueForMissingStub: null,
);
}

View file

@ -79,6 +79,7 @@ export class AlbumRepository implements IAlbumRepository {
const queryProperties: FindManyOptions<AlbumEntity> = {
relations: { sharedUsers: true, assets: true, sharedLinks: true, owner: true },
select: { assets: { id: true } },
order: { assets: { fileCreatedAt: 'ASC' }, createdAt: 'ASC' },
};
@ -112,10 +113,6 @@ export class AlbumRepository implements IAlbumRepository {
});
}
const albums = await albumsQuery;
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
return albumsQuery;
}

View file

@ -66,11 +66,11 @@ export class AlbumService {
*/
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
let albums: AlbumEntity[];
if (typeof getAlbumsDto.assetId === 'string') {
albums = await this.albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
} else {
albums = await this.albumRepository.getList(authUser.id, getAlbumsDto);
if (getAlbumsDto.shared) {
const publicSharingAlbums = await this.albumRepository.getPublicSharingList(authUser.id);
albums = [...albums, ...publicSharingAlbums];

View file

@ -18,7 +18,7 @@ export class AlbumEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
owner!: UserEntity;
@Column()
@ -36,11 +36,11 @@ export class AlbumEntity {
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
albumThumbnailAssetId!: string | null;
@ManyToMany(() => UserEntity, { eager: true })
@ManyToMany(() => UserEntity)
@JoinTable()
sharedUsers!: UserEntity[];
@ManyToMany(() => AssetEntity, { eager: true })
@ManyToMany(() => AssetEntity)
@JoinTable()
assets!: AssetEntity[];

View file

@ -27,7 +27,7 @@ export class AssetEntity {
@Column()
deviceAssetId!: string;
@ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
owner!: UserEntity;
@Column()
@ -92,11 +92,11 @@ export class AssetEntity {
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
smartInfo?: SmartInfoEntity;
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true, eager: true })
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
@JoinTable({ name: 'tag_asset' })
tags!: TagEntity[];
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true, eager: true })
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
@JoinTable({ name: 'shared_link__asset' })
sharedLinks!: SharedLinkEntity[];
}

View file

@ -52,6 +52,7 @@ export class SharedLinkEntity {
@ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks)
assets!: AssetEntity[];
@Index('IDX_sharedlink_albumId')
@ManyToOne(() => AlbumEntity, (album) => album.sharedLinks)
album?: AlbumEntity;
}

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddIndexForAlbumInSharedLinkTable1677535643119 implements MigrationInterface {
name = 'AddIndexForAlbumInSharedLinkTable1677535643119'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE INDEX "IDX_sharedlink_albumId" ON "shared_links" ("albumId") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_albumId"`);
}
}

View file

@ -26,6 +26,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
assets: {
exifInfo: true,
},
owner: true,
},
},
order: {
@ -49,7 +50,9 @@ export class SharedLinkRepository implements ISharedLinkRepository {
},
relations: {
assets: true,
album: true,
album: {
owner: true,
},
},
order: {
createdAt: 'DESC',

View file

@ -1,64 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte';
export let localVersion: string;
export let remoteVersion: string;
const dispatch = createEventDispatcher();
const acknowledgeClickHandler = () => {
localStorage.setItem('appVersion', remoteVersion);
dispatch('close');
};
</script>
<div class="absolute top-0 left-0 w-screen h-screen">
<FullScreenModal on:clickOutside={() => console.log('Click outside')}>
<div class="max-w-[500px] max-w-[95vw] z-[99999] border bg-immich-bg p-10 rounded-xl">
<p class="text-2xl ">🎉 NEW VERSION AVAILABLE 🎉</p>
<br />
<section class="max-h-[400px] overflow-y-auto">
<div class="font-thin">
Hi friend, there is a new release of <span
class="font-immich-title text-immich-primary font-bold">IMMICH</span
>, please take your time to visit the
<span class="underline font-medium"
><a
href="https://github.com/immich-app/immich/releases/latest"
target="_blank"
rel="noopener noreferrer">release note</a
></span
>
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent
any misconfigurations, especially if you use WatchTower or any mechanism that handles updating
your application automatically.
</div>
{#if remoteVersion == 'v1.11.0_17-dev'}
<div class="mt-2 font-thin">
This specific version <span class="font-medium">v1.11.0_17-dev</span> includes changes in
the docker-compose setup that added additional containters. Please make sure to update the
docker-compose file, pull new images and check your setup for the latest features and bug
fixes.
</div>
{/if}
</section>
<div class="font-thin mt-4">Your friend, Alex</div>
<div class="text-xs mt-8">
<code>Local Version {localVersion}</code>
<br />
<code>Remote Version {remoteVersion}</code>
</div>
<div class="text-right mt-4">
<button
class="bg-immich-primary text-gray-50 hover:bg-immich-primary/90 py-2 px-4 rounded-lg font-medium shadow-lg transition-all"
on:click={acknowledgeClickHandler}>Acknowledge</button
>
</div>
</div>
</FullScreenModal>
</div>

View file

@ -0,0 +1,82 @@
<script lang="ts">
import { getGithubVersion } from '$lib/utils/get-github-version';
import { onMount } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte';
import type { ServerVersionReponseDto } from '@api';
export let serverVersion: ServerVersionReponseDto;
let showModal = false;
let githubVersion: string;
$: serverVersionName = semverToName(serverVersion);
function semverToName({ major, minor, patch }: ServerVersionReponseDto) {
return `v${major}.${minor}.${patch}`;
}
function onAcknowledge() {
// Store server version to prevent the notification
// from showing again.
localStorage.setItem('appVersion', githubVersion);
showModal = false;
}
onMount(async () => {
try {
githubVersion = await getGithubVersion();
if (localStorage.getItem('appVersion') === githubVersion) {
// Updated version has already been acknowledged.
return;
}
if (githubVersion !== serverVersionName) {
showModal = true;
}
} catch (err) {
// Only log any errors that occur.
console.error('Error [VersionAnnouncementBox]:', err);
}
});
</script>
{#if showModal}
<FullScreenModal on:clickOutside={() => (showModal = false)}>
<div
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray shadow-sm max-w-lg rounded-3xl py-10 px-8 dark:text-immich-dark-fg "
>
<p class="text-2xl mb-4">🎉 NEW VERSION AVAILABLE 🎉</p>
<div>
Hi friend, there is a new release of
<span class="font-immich-title text-immich-primary dark:text-immich-dark-primary font-bold"
>IMMICH</span
>, please take your time to visit the
<span class="underline font-medium"
><a
href="https://github.com/immich-app/immich/releases/latest"
target="_blank"
rel="noopener noreferrer">release notes</a
></span
>
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent
any misconfigurations, especially if you use WatchTower or any mechanism that handles updating
your application automatically.
</div>
<div class="font-medium mt-4">Your friend, Alex</div>
<div class="font-sm mt-8">
<code>Server Version: {serverVersionName}</code>
<br />
<code>Latest Version: {githubVersion}</code>
</div>
<div class="text-right mt-8">
<button
class="transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
on:click={onAcknowledge}>Acknowledge</button
>
</div>
</div>
</FullScreenModal>
{/if}

View file

@ -1,50 +0,0 @@
type CheckAppVersionReponse = {
shouldShowAnnouncement: boolean;
localVersion?: string;
remoteVersion?: string;
};
type GithubRelease = {
tag_name: string;
};
export const checkAppVersion = async (): Promise<CheckAppVersionReponse> => {
const res = await fetch('https://api.github.com/repos/immich-app/immich/releases/latest', {
headers: {
Accept: 'application/vnd.github.v3+json'
}
});
if (res.status == 200) {
const latestRelease = (await res.json()) as GithubRelease;
const appVersion = localStorage.getItem('appVersion');
if (!appVersion) {
return {
shouldShowAnnouncement: false,
remoteVersion: latestRelease.tag_name,
localVersion: 'empty'
};
}
if (appVersion != latestRelease.tag_name) {
return {
shouldShowAnnouncement: true,
remoteVersion: latestRelease.tag_name,
localVersion: appVersion
};
}
return {
shouldShowAnnouncement: false,
remoteVersion: latestRelease.tag_name,
localVersion: appVersion
};
} else {
return {
shouldShowAnnouncement: false,
remoteVersion: '0',
localVersion: '0'
};
}
};

View file

@ -0,0 +1,18 @@
import axios from 'axios';
type GithubRelease = {
tag_name: string;
};
export const getGithubVersion = async (): Promise<string> => {
const { data } = await axios.get<GithubRelease>(
'https://api.github.com/repos/immich-app/immich/releases/latest',
{
headers: {
Accept: 'application/vnd.github.v3+json'
}
}
);
return data.tag_name;
};

View file

@ -1,7 +1,6 @@
<script lang="ts">
import AlbumCard from '$lib/components/album-page/album-card.svelte';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
@ -20,13 +19,10 @@
contextMenuPosition,
createAlbum,
deleteSelectedContextAlbum,
loadAlbums,
showAlbumContextMenu,
closeAlbumContextMenu
} = useAlbums({ albums: data.albums });
onMount(loadAlbums);
const handleCreateAlbum = async () => {
const newAlbum = await createAlbum();
if (newAlbum) {

View file

@ -1,5 +1,7 @@
import type { LayoutServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
return { user };
export const load = (async ({ locals: { api, user } }) => {
const { data: serverVersion } = await api.serverInfoApi.getServerVersion();
return { serverVersion, user };
}) satisfies LayoutServerLoad;

View file

@ -7,7 +7,9 @@
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte';
import faviconUrl from '$lib/assets/favicon.png';
import type { LayoutData } from './$types';
let showNavigationLoadingBar = false;
@ -18,6 +20,8 @@
afterNavigate(() => {
showNavigationLoadingBar = false;
});
export let data: LayoutData;
</script>
<svelte:head>
@ -50,3 +54,7 @@
<DownloadPanel />
<UploadPanel />
<NotificationList />
{#if data.user?.isAdmin}
<VersionAnnouncementBox serverVersion={data.serverVersion} />
{/if}