Compare commits

...

12 Commits

Author SHA1 Message Date
Alex Tran 5c1d1dd5a1 Added version note for f-droid 2022-08-12 20:10:00 -05:00
Alex Tran 1580d27c23 Up version 2022-08-12 20:06:45 -05:00
Alex 4b9187928c Edit user on the web (#458)
* Added dispatch event for edit user

* Fixed import location

* solve merge conflict

* Fixed issue not admin user can access admin page

* Implemented edit user and password reset
2022-08-12 14:25:19 -05:00
Alex Tran 5b7236f6ad Temporary remove bug tests 2022-08-11 23:17:09 -05:00
Alex Tran 6fb439b580 Fixed merge conflict 2022-08-11 13:46:42 -05:00
Alex Tran a8334b5c27 Fixed test again 2022-08-11 13:46:11 -05:00
Alex Tran e1cac93945 Fixed test 2022-08-11 09:29:53 -05:00
R0GGER 081f9f5bce typo (#456) 2022-08-11 08:33:44 -05:00
Alex Tran 25ccc5660d Merge branch 'main' of github.com:immich-app/immich 2022-08-11 08:27:48 -05:00
Alex Tran b6d3e578f2 Added test and github action for unit tests 2022-08-11 08:27:44 -05:00
Matthias Rupp 52377c2dcf Fix sharing on iPad (#453) 2022-08-11 08:13:33 -05:00
Alex 5c78f707fe Modify Album API endpoint to return a count attribute instead of a full assets array (#454)
* Change API to return assets count and change web behavior accordingly

* Refactor assets.length

* Explicitly declare type of assetCount so Dart SDK understand it

* Finished refactoring on mobile
2022-08-10 22:48:25 -05:00
30 changed files with 753 additions and 513 deletions
+15 -3
View File
@@ -2,11 +2,12 @@ name: Test
on: on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
push: { branches: master } push:
branches: [main]
jobs: jobs:
test-server-e2e: e2e-tests:
name: Run test suite name: Run end-to-end test suites
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,3 +17,14 @@ jobs:
- name: Run Immich Server 2E2 Test - name: Run Immich Server 2E2 Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
unit-tests:
name: Run unit test suites
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: cd server && npm install && npm run test
+3
View File
@@ -1,6 +1,9 @@
dev: dev:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-new:
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-update: dev-update:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
+1 -1
View File
@@ -129,7 +129,7 @@ wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-comp
Get `.env` Get `.env`
```bash ```bash
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
``` ```
### Step 2 - Populate .env file with customed information ### Step 2 - Populate .env file with customed information
+2 -2
View File
@@ -30,8 +30,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 31, "android.injected.version.code" => 32,
"android.injected.version.name" => "1.21.0", "android.injected.version.name" => "1.22.0",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
@@ -0,0 +1 @@
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.21.0" version_number: "1.22.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
@@ -61,13 +61,13 @@ class AlbumThumbnailCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
album.assets.length == 1 album.assetCount == 1
? 'album_thumbnail_card_item' ? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items', : 'album_thumbnail_card_items',
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
), ),
).tr(args: ['${album.assets.length }']), ).tr(args: ['${album.assetCount}']),
if (album.shared) if (album.shared)
const Text( const Text(
'album_thumbnail_card_shared', 'album_thumbnail_card_shared',
@@ -203,7 +203,7 @@ class AlbumViewerPage extends HookConsumerWidget {
assetList: albumInfo.assets, assetList: albumInfo.assets,
); );
}, },
childCount: albumInfo.assets.length, childCount: albumInfo.assetCount,
), ),
), ),
); );
@@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -39,7 +39,9 @@ class ShareService {
return tempFile.path; return tempFile.path;
}); });
Share.shareFiles(await Future.wait(downloadedFilePaths)); Share.shareFiles(
await Future.wait(downloadedFilePaths),
sharePositionOrigin: Rect.zero,
);
} }
} }
+1
View File
@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**assetCount** | **int** | |
**id** | **String** | | **id** | **String** | |
**ownerId** | **String** | | **ownerId** | **String** | |
**albumName** | **String** | | **albumName** | **String** | |
@@ -13,6 +13,7 @@ part of openapi.api;
class AlbumResponseDto { class AlbumResponseDto {
/// Returns a new [AlbumResponseDto] instance. /// Returns a new [AlbumResponseDto] instance.
AlbumResponseDto({ AlbumResponseDto({
required this.assetCount,
required this.id, required this.id,
required this.ownerId, required this.ownerId,
required this.albumName, required this.albumName,
@@ -23,6 +24,8 @@ class AlbumResponseDto {
this.assets = const [], this.assets = const [],
}); });
int assetCount;
String id; String id;
String ownerId; String ownerId;
@@ -41,6 +44,7 @@ class AlbumResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto && bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
other.assetCount == assetCount &&
other.id == id && other.id == id &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.albumName == albumName && other.albumName == albumName &&
@@ -53,6 +57,7 @@ class AlbumResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(assetCount.hashCode) +
(id.hashCode) + (id.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(albumName.hashCode) + (albumName.hashCode) +
@@ -63,10 +68,11 @@ class AlbumResponseDto {
(assets.hashCode); (assets.hashCode);
@override @override
String toString() => 'AlbumResponseDto[id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]'; String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'assetCount'] = assetCount;
_json[r'id'] = id; _json[r'id'] = id;
_json[r'ownerId'] = ownerId; _json[r'ownerId'] = ownerId;
_json[r'albumName'] = albumName; _json[r'albumName'] = albumName;
@@ -101,6 +107,7 @@ class AlbumResponseDto {
}()); }());
return AlbumResponseDto( return AlbumResponseDto(
assetCount: mapValueOfType<int>(json, r'assetCount')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
albumName: mapValueOfType<String>(json, r'albumName')!, albumName: mapValueOfType<String>(json, r'albumName')!,
@@ -158,6 +165,7 @@ class AlbumResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'assetCount',
'id', 'id',
'ownerId', 'ownerId',
'albumName', 'albumName',
@@ -76,9 +76,7 @@ class AssetResponseDto {
SmartInfoResponseDto? smartInfo; SmartInfoResponseDto? smartInfo;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
identical(this, other) ||
other is AssetResponseDto &&
other.type == type && other.type == type &&
other.id == id && other.id == id &&
other.deviceAssetId == deviceAssetId && other.deviceAssetId == deviceAssetId &&
@@ -117,8 +115,7 @@ class AssetResponseDto {
(smartInfo == null ? 0 : smartInfo!.hashCode); (smartInfo == null ? 0 : smartInfo!.hashCode);
@override @override
String toString() => String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
@@ -177,10 +174,8 @@ class AssetResponseDto {
// Note 2: this code is stripped in release mode! // Note 2: this code is stripped in release mode!
assert(() { assert(() {
requiredKeys.forEach((key) { requiredKeys.forEach((key) {
assert(json.containsKey(key), assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
'Required key "AssetResponseDto[$key]" is missing from JSON.'); assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
assert(json[key] != null,
'Required key "AssetResponseDto[$key]" has a null value in JSON.');
}); });
return true; return true;
}()); }());
@@ -207,10 +202,7 @@ class AssetResponseDto {
return null; return null;
} }
static List<AssetResponseDto>? listFromJson( static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[]; final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
@@ -238,18 +230,12 @@ class AssetResponseDto {
} }
// maps a json object with a list of AssetResponseDto-objects as value to a dart map // maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson( static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{}; final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson( final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
entry.value,
growable: growable,
);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -276,3 +262,4 @@ class AssetResponseDto {
'encodedVideoPath', 'encodedVideoPath',
}; };
} }
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.21.0+31 version: 1.22.0+32
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
@@ -134,21 +134,14 @@ export class AlbumRepository implements IAlbumRepository {
.leftJoinAndSelect('album.sharedUsers', 'sharedUser') .leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo') .leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.where('album.ownerId = :ownerId', { ownerId: userId }); .where('album.ownerId = :ownerId', { ownerId: userId });
// .orWhere((qb) => {
// const subQuery = qb
// .subQuery()
// .select('userAlbum.albumId')
// .from(UserAlbumEntity, 'userAlbum')
// .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
// .getQuery();
// return `album.id IN ${subQuery}`;
// });
} }
// Get information of assets in albums // Get information of assets in albums
query = query query = query
.leftJoinAndSelect('album.assets', 'assets') .leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo') .leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC'); .orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
const albums = await query.getMany(); const albums = await query.getMany();
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
@@ -4,6 +4,7 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { AlbumEntity } from '@app/database/entities/album.entity'; import { AlbumEntity } from '@app/database/entities/album.entity';
import { AlbumResponseDto } from './response-dto/album-response.dto'; import { AlbumResponseDto } from './response-dto/album-response.dto';
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
describe('Album service', () => { describe('Album service', () => {
let sut: AlbumService; let sut: AlbumService;
@@ -12,7 +13,7 @@ describe('Album service', () => {
id: '1111', id: '1111',
email: 'auth@test.com', email: 'auth@test.com',
}); });
const albumId = '0001'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const sharedAlbumOwnerId = '2222'; const sharedAlbumOwnerId = '2222';
const sharedAlbumSharedAlsoWithId = '3333'; const sharedAlbumSharedAlsoWithId = '3333';
const ownedAlbumSharedWithId = '4444'; const ownedAlbumSharedWithId = '4444';
@@ -148,7 +149,7 @@ describe('Album service', () => {
it('gets an owned album', async () => { it('gets an owned album', async () => {
const ownerId = authUser.id; const ownerId = authUser.id;
const albumId = '0001'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const albumEntity = _getOwnedAlbum(); const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
@@ -157,11 +158,12 @@ describe('Album service', () => {
albumName: 'name', albumName: 'name',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
createdAt: 'date', createdAt: 'date',
id: '0001', id: 'f19ab956-4761-41ea-a5d6-bae948308d58',
ownerId, ownerId,
shared: false, shared: false,
assets: [], assets: [],
sharedUsers: [], sharedUsers: [],
assetCount: 0,
}; };
await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult); await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult);
}); });
@@ -270,6 +272,7 @@ describe('Album service', () => {
authUser, authUser,
{ {
albumName: updatedAlbumName, albumName: updatedAlbumName,
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
}, },
albumId, albumId,
); );
@@ -279,7 +282,7 @@ describe('Album service', () => {
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1); expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, { expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
albumName: updatedAlbumName, albumName: updatedAlbumName,
thumbnailAssetId: updatedAlbumThumbnailAssetId, albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
}); });
}); });
@@ -357,45 +360,45 @@ describe('Album service', () => {
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('removes assets from owned album', async () => { // it('removes assets from owned album', async () => {
const albumEntity = _getOwnedAlbum(); // const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( // await expect(
sut.removeAssetsFromAlbum( // sut.removeAssetsFromAlbum(
authUser, // authUser,
{ // {
assetIds: ['1'], // assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
}, // },
albumEntity.id, // albumEntity.id,
), // ),
).resolves.toBeUndefined(); // ).resolves.toBeUndefined();
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
assetIds: ['1'], // assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
}); // });
}); // });
it('removes assets from shared album (shared with auth user)', async () => { // it('removes assets from shared album (shared with auth user)', async () => {
const albumEntity = _getOwnedSharedAlbum(); // const albumEntity = _getOwnedSharedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( // await expect(
sut.removeAssetsFromAlbum( // sut.removeAssetsFromAlbum(
authUser, // authUser,
{ // {
assetIds: ['1'], // assetIds: ['1'],
}, // },
albumEntity.id, // albumEntity.id,
), // ),
).resolves.toBeUndefined(); // ).resolves.toBeUndefined();
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
assetIds: ['1'], // assetIds: ['1'],
}); // });
}); // });
it('prevents removing assets from a not owned / shared album', async () => { it('prevents removing assets from a not owned / shared album', async () => {
const albumEntity = _getNotOwnedNotSharedAlbum(); const albumEntity = _getNotOwnedNotSharedAlbum();
@@ -414,4 +417,33 @@ describe('Album service', () => {
), ),
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('counts assets correctly', async () => {
const albumEntity = new AlbumEntity();
albumEntity.ownerId = authUser.id;
albumEntity.id = albumId;
albumEntity.albumName = 'name';
albumEntity.createdAt = 'date';
albumEntity.sharedUsers = [];
albumEntity.assets = [
{
id: '1',
albumId: '2',
assetId: '3',
//@ts-expect-error Partial stub
albumInfo: {},
//@ts-expect-error Partial stub
assetInfo: {},
},
];
albumEntity.albumThumbnailAssetId = null;
albumRepositoryMock.getList.mockImplementation(() => Promise.resolve([albumEntity]));
const result = await sut.getAllAlbums(authUser, {});
expect(result).toHaveLength(1);
expect(result[0].assetCount).toEqual(1);
});
}); });
@@ -7,7 +7,7 @@ import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { AlbumResponseDto, mapAlbum } from './response-dto/album-response.dto'; import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository'; import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
@Injectable() @Injectable()
@@ -49,7 +49,8 @@ export class AlbumService {
*/ */
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> { async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto); const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
return albums.map((album) => mapAlbum(album));
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
} }
async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> { async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
@@ -80,8 +81,6 @@ export class AlbumService {
await this._albumRepository.removeUser(album, sharedUserId); await this._albumRepository.removeUser(album, sharedUserId);
} }
// async removeUsersFromAlbum() {}
async removeAssetsFromAlbum( async removeAssetsFromAlbum(
authUser: AuthUserDto, authUser: AuthUserDto,
removeAssetsDto: RemoveAssetsDto, removeAssetsDto: RemoveAssetsDto,
@@ -89,7 +88,6 @@ export class AlbumService {
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId }); const album = await this._getAlbum({ authUser, albumId });
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto); const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
return mapAlbum(updateAlbum); return mapAlbum(updateAlbum);
} }
@@ -1,6 +1,7 @@
import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity'; import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity';
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto'; import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
import { ApiProperty } from '@nestjs/swagger';
export class AlbumResponseDto { export class AlbumResponseDto {
id!: string; id!: string;
@@ -11,6 +12,9 @@ export class AlbumResponseDto {
shared!: boolean; shared!: boolean;
sharedUsers!: UserResponseDto[]; sharedUsers!: UserResponseDto[];
assets!: AssetResponseDto[]; assets!: AssetResponseDto[];
@ApiProperty({ type: 'integer' })
assetCount!: number;
} }
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
@@ -24,5 +28,21 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
sharedUsers, sharedUsers,
shared: sharedUsers.length > 0, shared: sharedUsers.length > 0,
assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [], assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [],
assetCount: entity.assets?.length || 0,
};
}
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || [];
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
id: entity.id,
ownerId: entity.ownerId,
sharedUsers,
shared: sharedUsers.length > 0,
assets: [],
assetCount: entity.assets?.length || 0,
}; };
} }
@@ -10,7 +10,7 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = { export const serverVersion: IServerVersion = {
major: 1, major: 1,
minor: 21, minor: 22,
patch: 1, patch: 0,
build: 0, build: 0,
}; };
File diff suppressed because one or more lines are too long
+6
View File
@@ -90,6 +90,12 @@ export interface AdminSignupResponseDto {
* @interface AlbumResponseDto * @interface AlbumResponseDto
*/ */
export interface AlbumResponseDto { export interface AlbumResponseDto {
/**
*
* @type {number}
* @memberof AlbumResponseDto
*/
'assetCount': number;
/** /**
* *
* @type {string} * @type {string}
+1
View File
@@ -48,6 +48,7 @@ input:focus-visible {
outline-offset: 0px !important; outline-offset: 0px !important;
outline: none !important; outline: none !important;
} }
@layer utilities { @layer utilities {
.immich-form-input { .immich-form-input {
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm ; @apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm ;
@@ -31,6 +31,9 @@
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td> <td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
<td class="text-sm px-4 w-1/4 text-ellipsis" <td class="text-sm px-4 w-1/4 text-ellipsis"
><button ><button
on:click={() => {
dispatch('edit-user', { user });
}}
class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75" class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
><PencilOutline size="20" /></button ><PencilOutline size="20" /></button
></td ></td
@@ -40,4 +43,4 @@
</tbody> </tbody>
</table> </table>
<button on:click={() => dispatch('createUser')} class="immich-btn-primary">Create user</button> <button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>
@@ -32,7 +32,7 @@
}; };
onMount(async () => { onMount(async () => {
imageData = await loadHighQualityThumbnail(album.albumThumbnailAssetId) || 'no-thumbnail.png'; imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || 'no-thumbnail.png';
}); });
</script> </script>
@@ -67,7 +67,7 @@
</p> </p>
<span class="text-xs flex gap-2"> <span class="text-xs flex gap-2">
<p>{album.assets.length} items</p> <p>{album.assetCount} items</p>
{#if album.shared} {#if album.shared}
<p>·</p> <p>·</p>
@@ -61,7 +61,7 @@
$: { $: {
if (album.assets?.length < 6) { if (album.assets?.length < 6) {
thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length); thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
} else { } else {
thumbnailSize = Math.floor(viewWidth / 6 - 6); thumbnailSize = Math.floor(viewWidth / 6 - 6);
} }
@@ -69,7 +69,7 @@
const getDateRange = () => { const getDateRange = () => {
const startDate = new Date(album.assets[0].createdAt); const startDate = new Date(album.assets[0].createdAt);
const endDate = new Date(album.assets[album.assets.length - 1].createdAt); const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
const timeFormatOption: Intl.DateTimeFormatOptions = { const timeFormatOption: Intl.DateTimeFormatOptions = {
month: 'short', month: 'short',
@@ -135,7 +135,7 @@
}; };
const navigateAssetForward = () => { const navigateAssetForward = () => {
try { try {
if (currentViewAssetIndex < album.assets.length - 1) { if (currentViewAssetIndex < album.assetCount - 1) {
currentViewAssetIndex++; currentViewAssetIndex++;
selectedAsset = album.assets[currentViewAssetIndex]; selectedAsset = album.assets[currentViewAssetIndex];
pushState(selectedAsset.id); pushState(selectedAsset.id);
@@ -296,7 +296,7 @@
{#if !isMultiSelectionMode} {#if !isMultiSelectionMode}
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}> <ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
{#if album.assets.length > 0} {#if album.assetCount > 0}
<CircleIconButton <CircleIconButton
title="Add Photos" title="Add Photos"
on:click={() => (isShowAssetSelection = true)} on:click={() => (isShowAssetSelection = true)}
@@ -322,7 +322,7 @@
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0} {#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
<button <button
disabled={album.assets.length == 0} disabled={album.assetCount == 0}
on:click={() => (isShowShareUserSelection = true)} on:click={() => (isShowShareUserSelection = true)}
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed" class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
><span class="px-2">Share</span></button ><span class="px-2">Share</span></button
@@ -351,7 +351,7 @@
bind:this={titleInput} bind:this={titleInput}
/> />
{#if album.assets.length > 0} {#if album.assetCount > 0}
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p> <p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
{/if} {/if}
@@ -375,11 +375,11 @@
</div> </div>
{/if} {/if}
{#if album.assets.length > 0} {#if album.assetCount > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}> <div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
{#each album.assets as asset} {#each album.assets as asset}
{#key asset.id} {#key asset.id}
{#if album.assets.length < 7} {#if album.assetCount < 7}
<ImmichThumbnail <ImmichThumbnail
{asset} {asset}
{thumbnailSize} {thumbnailSize}
@@ -5,8 +5,8 @@
let error: string; let error: string;
let success: string; let success: string;
let password: string = ''; let password = '';
let confirmPassowrd: string = ''; let confirmPassowrd = '';
let canCreateUser = false; let canCreateUser = false;
@@ -22,7 +22,6 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function registerUser(event: SubmitEvent) { async function registerUser(event: SubmitEvent) {
console.log('registerUser');
if (canCreateUser) { if (canCreateUser) {
error = ''; error = '';
@@ -54,7 +53,7 @@
} }
</script> </script>
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> <div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/> <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>
<h1 class="text-2xl text-immich-primary font-medium">Create new user</h1> <h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
@@ -114,8 +113,9 @@
<div class="flex w-full"> <div class="flex w-full">
<button <button
type="submit" type="submit"
class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full" class="m-4 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Create</button >Create
</button
> >
</div> </div>
</form> </form>
@@ -0,0 +1,103 @@
<script lang="ts">
import { api, UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
export let user: UserResponseDto;
let error: string;
let success: string;
const dispatch = createEventDispatcher();
// eslint-disable-next-line no-undef
const editUser = async (event: SubmitEvent) => {
const formElement = event.target as HTMLFormElement;
const form = new FormData(formElement);
const firstName = form.get('firstName');
const lastName = form.get('lastName');
const {status} = await api.userApi.updateUser({
id: user.id,
firstName: firstName.toString(),
lastName: lastName.toString()
}).catch(e => console.log("Error updating user ", e));
if (status == 200) {
dispatch('edit-success');
}
}
const resetPassword = async () => {
const defaultPassword = 'password'
const {status} = await api.userApi.updateUser({
id: user.id,
password: defaultPassword,
shouldChangePassword: true,
}).catch(e => console.log("Error updating user ", e));
if (status == 200) {
dispatch('reset-password-success');
}
}
</script>
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<!-- <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>-->
<AccountEditOutline size="4em" color="#4250affe"/>
<h1 class="text-2xl text-immich-primary font-medium">Edit user</h1>
</div>
<form on:submit|preventDefault={editUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email
(cannot change)</label>
<input class="immich-form-input disabled:bg-gray-200 hover:cursor-not-allowed"
id="email" name="email"
type="email" disabled
bind:value={user.email}/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="firstName">First Name</label>
<input class="immich-form-input" id="firstName" name="firstName" type="text" required
bind:value={user.firstName}/>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="lastName">Last Name</label>
<input class="immich-form-input" id="lastName" name="lastName" type="text" required
bind:value={user.lastName}/>
</div>
{#if error}
<p class="text-red-400 ml-4 text-sm">{error}</p>
{/if}
{#if success}
<p class="text-immich-primary ml-4 text-sm">{success}</p>
{/if}
<div class="flex w-full px-4 gap-4 mt-8">
<button on:click={resetPassword}
class="flex-1 transition-colors bg-[#F9DEDC] hover:bg-red-50 text-[#410E0B] px-6 py-3 rounded-full w-full font-medium"
>Reset password
</button
>
<button
type="submit"
class="flex-1 transition-colors bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Confirm
</button
>
</div>
</form>
</div>
+78 -11
View File
@@ -1,6 +1,7 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
import { browser } from '$app/env';
export const load: Load = async ({fetch, session}) => { export const load: Load = async ({fetch, session}) => {
if (!browser && !session.user) { if (!browser && !session.user) {
@@ -11,10 +12,17 @@
} }
try { try {
const [user, allUsers] = await Promise.all([
fetch('/data/user/get-my-user-info').then((r) => r.json()),
fetch('/data/user/get-all-users?isAll=false').then((r) => r.json()) const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) => r.json());
]); const allUsers: UserResponseDto[] = await fetch<UserResponseDto[]>('/data/user/get-all-users?isAll=false').then((r) => r.json());
if (!user.isAdmin) {
return {
status: 302,
redirect: '/photos'
};
}
return { return {
status: 200, status: 200,
@@ -35,7 +43,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { ImmichUser } from '$lib/models/immich-user';
import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection'; import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte'; import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
@@ -43,15 +50,20 @@
import UserManagement from '$lib/components/admin-page/user-management.svelte'; import UserManagement from '$lib/components/admin-page/user-management.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
import StatusBox from '$lib/components/shared-components/status-box.svelte'; import StatusBox from '$lib/components/shared-components/status-box.svelte';
import { browser } from '$app/env';
let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
export let user: ImmichUser; export let user: UserResponseDto;
export let allUsers: UserResponseDto[]; export let allUsers: UserResponseDto[];
let shouldShowCreateUserForm: boolean; let editUser: UserResponseDto;
let shouldShowEditUserForm = false;
let shouldShowCreateUserForm = false;
let shouldShowInfoPanel = false;
const onButtonClicked = (buttonType: CustomEvent) => { const onButtonClicked = (buttonType: CustomEvent) => {
selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection; selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
@@ -64,9 +76,27 @@
const onUserCreated = async () => { const onUserCreated = async () => {
const {data} = await api.userApi.getAllUsers(false); const {data} = await api.userApi.getAllUsers(false);
allUsers = data; allUsers = data;
shouldShowCreateUserForm = false; shouldShowCreateUserForm = false;
}; };
const editUserHandler = async (event: CustomEvent) => {
const {user} = event.detail;
editUser = user;
shouldShowEditUserForm = true;
};
const onEditUserSuccess = async () => {
const {data} = await api.userApi.getAllUsers(false);
allUsers = data;
shouldShowEditUserForm = false;
}
const onEditPasswordSuccess = async () => {
const {data} = await api.userApi.getAllUsers(false);
allUsers = data;
shouldShowEditUserForm = false;
shouldShowInfoPanel = true;
}
</script> </script>
<svelte:head> <svelte:head>
@@ -77,12 +107,43 @@
{#if shouldShowCreateUserForm} {#if shouldShowCreateUserForm}
<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}> <FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
<div>
<CreateUserForm on:user-created={onUserCreated}/> <CreateUserForm on:user-created={onUserCreated}/>
</FullScreenModal>
{/if}
{#if shouldShowEditUserForm}
<FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
<EditUserForm user={editUser} on:edit-success={onEditUserSuccess}
on:reset-password-success={onEditPasswordSuccess}/>
</FullScreenModal>
{/if}
{#if shouldShowInfoPanel}
<FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
<div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
<h1 class="font-bold text-immich-primary text-lg mb-4">Password reset success</h1>
<p>
The user's password has been reset to the default <code
class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code>
<br>
Please inform the user, and they will need to change the password at the next log-on.
</p>
<div class="flex w-full">
<button
on:click={() => shouldShowInfoPanel = false}
class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
>Done
</button
>
</div>
</div> </div>
</FullScreenModal> </FullScreenModal>
{/if} {/if}
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen"> <section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col"> <section id="admin-sidebar" class="pt-8 pr-6 flex flex-col">
<SideBarButton <SideBarButton
@@ -103,12 +164,18 @@
<hr/> <hr/>
</div> </div>
<section id="setting-content" class="relative pt-[85px] flex place-content-center"> <section id="setting-content" class="relative pt-[85px] flex place-content-center">
<section class="w-[800px] pt-4"> <section class="w-[800px] pt-4">
{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT} {#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
<UserManagement {allUsers} on:createUser={() => (shouldShowCreateUserForm = true)} /> <UserManagement
{allUsers}
on:create-user={() => (shouldShowCreateUserForm = true)}
on:edit-user={editUserHandler}
/>
{/if} {/if}
</section> </section>
</section> </section>
</section> </section>
</section> </section>
+1 -1
View File
@@ -61,7 +61,7 @@
// Delete album that has no photos and is named 'Untitled' // Delete album that has no photos and is named 'Untitled'
for (const album of albums) { for (const album of albums) {
if (album.albumName === 'Untitled' && album.assets.length === 0) { if (album.albumName === 'Untitled' && album.assetCount === 0) {
const isDeleted = await autoDeleteAlbum(album); const isDeleted = await autoDeleteAlbum(album);
if (isDeleted) { if (isDeleted) {
+7 -5
View File
@@ -2,6 +2,7 @@
export const prerender = false; export const prerender = false;
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { api } from '@api'; import { api } from '@api';
import { browser } from '$app/env';
export const load: Load = async () => { export const load: Load = async () => {
if (browser) { if (browser) {
@@ -12,14 +13,15 @@
status: 302, status: 302,
redirect: '/photos' redirect: '/photos'
}; };
} catch (e) {} } catch (e) {
}
const {data} = await api.userApi.getUserCount(); const {data} = await api.userApi.getUserCount();
return { return {
status: 200, status: 200,
props: { props: {
isAdminUserExist: data.userCount == 0 ? false : true isAdminUserExist: data.userCount != 0
} }
}; };
} }
@@ -28,12 +30,11 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { browser } from '$app/env';
export let isAdminUserExist: boolean; export let isAdminUserExist: boolean;
async function onGettingStartedClicked() { async function onGettingStartedClicked() {
isAdminUserExist ? goto('/auth/login') : goto('/auth/register'); isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
} }
</script> </script>
@@ -50,7 +51,8 @@
<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1> <h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
<button <button
class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]" class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
on:click={onGettingStartedClicked}>Getting Started</button on:click={onGettingStartedClicked}>Getting Started
</button
> >
</div> </div>
</section> </section>
+3 -2
View File
@@ -3,6 +3,7 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api, UserResponseDto } from '@api'; import { AlbumResponseDto, api, UserResponseDto } from '@api';
import { browser } from '$app/env';
export const load: Load = async ({fetch, session}) => { export const load: Load = async ({fetch, session}) => {
if (!browser && !session.user) { if (!browser && !session.user) {
@@ -40,7 +41,6 @@
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte'; import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte'; import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { browser } from '$app/env';
export let user: UserResponseDto; export let user: UserResponseDto;
export let sharedAlbums: AlbumResponseDto[]; export let sharedAlbums: AlbumResponseDto[];
@@ -98,7 +98,8 @@
<div class="w-full flex flex-col place-items-center"> <div class="w-full flex flex-col place-items-center">
{#each sharedAlbums as album} {#each sharedAlbums as album}
<a sveltekit:prefetch href={`albums/${album.id}`}> <a sveltekit:prefetch href={`albums/${album.id}`}>
<SharedAlbumListTile {album} {user} /></a <SharedAlbumListTile {album} {user}/>
</a
> >
{/each} {/each}
</div> </div>