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:
commit
8abe6909ca
19 changed files with 522 additions and 139 deletions
|
@ -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) {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
104
mobile/test/favorite_provider_test.dart
Normal file
104
mobile/test/favorite_provider_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
259
mobile/test/favorite_provider_test.mocks.dart
Normal file
259
mobile/test/favorite_provider_test.mocks.dart
Normal 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,
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
|
@ -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'
|
||||
};
|
||||
}
|
||||
};
|
18
web/src/lib/utils/get-github-version.ts
Normal file
18
web/src/lib/utils/get-github-version.ts
Normal 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;
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue