mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 06:31:58 +00:00
Add ablum feature to web (#352)
* Added album page * Refactor sidebar * Added album assets count info * Added album viewer page * Refactor album sorting * Fixed incorrectly showing selected asset in album selection * Improve fetching speed with prefetch * Refactor to use ImmichThubmnail component for all * Update to the latest version of Svelte * Implement fixed app bar in album viewer * Added shared user avatar * Correctly get all owned albums, including shared
This commit is contained in:
parent
1887b5a860
commit
7134f93eb8
62 changed files with 2430 additions and 986 deletions
5
Makefile
5
Makefile
|
@ -17,4 +17,7 @@ prod:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
|
api:
|
||||||
|
cd ./server && npm run api:generate
|
|
@ -96,13 +96,16 @@ class MonthGroupTitle extends HookConsumerWidget {
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
GestureDetector(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
onTap: _handleTitleIconClick,
|
||||||
child: Text(
|
child: Padding(
|
||||||
_getSimplifiedMonth(),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 24,
|
_getSimplifiedMonth(),
|
||||||
color: Theme.of(context).primaryColor,
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -26,17 +26,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset) && !isAlbumExist) {
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
|
var isNewlySelected =
|
||||||
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
|
||||||
|
if (isSelected && !isAlbumExist) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
} else if (selectedAsset.contains(asset) && isAlbumExist) {
|
} else if (isSelected && isAlbumExist) {
|
||||||
return const Icon(
|
return const Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Color.fromARGB(255, 233, 233, 233),
|
color: Color.fromARGB(255, 233, 233, 233),
|
||||||
);
|
);
|
||||||
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
|
} else if (isNewlySelected && isAlbumExist) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
|
@ -50,17 +54,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxBorder drawBorderColor() {
|
BoxBorder drawBorderColor() {
|
||||||
if (selectedAsset.contains(asset) && !isAlbumExist) {
|
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
|
var isNewlySelected =
|
||||||
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
|
||||||
|
if (isSelected && !isAlbumExist) {
|
||||||
return Border.all(
|
return Border.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: Theme.of(context).primaryColorLight,
|
||||||
width: 10,
|
width: 10,
|
||||||
);
|
);
|
||||||
} else if (selectedAsset.contains(asset) && isAlbumExist) {
|
} else if (isSelected && isAlbumExist) {
|
||||||
return Border.all(
|
return Border.all(
|
||||||
color: const Color.fromARGB(255, 190, 190, 190),
|
color: const Color.fromARGB(255, 190, 190, 190),
|
||||||
width: 10,
|
width: 10,
|
||||||
);
|
);
|
||||||
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
|
} else if (isNewlySelected && isAlbumExist) {
|
||||||
return Border.all(
|
return Border.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: Theme.of(context).primaryColorLight,
|
||||||
width: 10,
|
width: 10,
|
||||||
|
@ -71,10 +79,15 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
var isSelected =
|
||||||
|
selectedAsset.map((item) => item.id).contains(asset.id);
|
||||||
|
var isNewlySelected =
|
||||||
|
newAssetsForAlbum.map((item) => item.id).contains(asset.id);
|
||||||
|
|
||||||
if (isAlbumExist) {
|
if (isAlbumExist) {
|
||||||
// Operation for existing album
|
// Operation for existing album
|
||||||
if (!selectedAsset.contains(asset)) {
|
if (!isSelected) {
|
||||||
if (newAssetsForAlbum.contains(asset)) {
|
if (isNewlySelected) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.removeSelectedAdditionalAssets([asset]);
|
.removeSelectedAdditionalAssets([asset]);
|
||||||
|
@ -86,7 +99,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Operation for new album
|
// Operation for new album
|
||||||
if (selectedAsset.contains(asset)) {
|
if (isSelected) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.removeSelectedNewAssets([asset]);
|
.removeSelectedNewAssets([asset]);
|
||||||
|
|
|
@ -37,6 +37,7 @@ doc/ServerPingResponse.md
|
||||||
doc/ServerVersionReponseDto.md
|
doc/ServerVersionReponseDto.md
|
||||||
doc/SignUpDto.md
|
doc/SignUpDto.md
|
||||||
doc/SmartInfoResponseDto.md
|
doc/SmartInfoResponseDto.md
|
||||||
|
doc/ThumbnailFormat.md
|
||||||
doc/UpdateAlbumDto.md
|
doc/UpdateAlbumDto.md
|
||||||
doc/UpdateDeviceInfoDto.md
|
doc/UpdateDeviceInfoDto.md
|
||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
|
@ -90,6 +91,7 @@ lib/model/server_ping_response.dart
|
||||||
lib/model/server_version_reponse_dto.dart
|
lib/model/server_version_reponse_dto.dart
|
||||||
lib/model/sign_up_dto.dart
|
lib/model/sign_up_dto.dart
|
||||||
lib/model/smart_info_response_dto.dart
|
lib/model/smart_info_response_dto.dart
|
||||||
|
lib/model/thumbnail_format.dart
|
||||||
lib/model/update_album_dto.dart
|
lib/model/update_album_dto.dart
|
||||||
lib/model/update_device_info_dto.dart
|
lib/model/update_device_info_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
|
@ -97,4 +99,4 @@ lib/model/user_count_response_dto.dart
|
||||||
lib/model/user_response_dto.dart
|
lib/model/user_response_dto.dart
|
||||||
lib/model/validate_access_token_response_dto.dart
|
lib/model/validate_access_token_response_dto.dart
|
||||||
pubspec.yaml
|
pubspec.yaml
|
||||||
test/validate_access_token_response_dto_test.dart
|
test/thumbnail_format_test.dart
|
||||||
|
|
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/doc/ThumbnailFormat.md
Normal file
BIN
mobile/openapi/doc/ThumbnailFormat.md
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/thumbnail_format.dart
Normal file
BIN
mobile/openapi/lib/model/thumbnail_format.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/test/thumbnail_format_test.dart
Normal file
BIN
mobile/openapi/test/thumbnail_format_test.dart
Normal file
Binary file not shown.
|
@ -84,7 +84,7 @@ export class AlbumRepository implements IAlbumRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
async getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
||||||
const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
|
const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
|
||||||
const userId = ownerId;
|
const userId = ownerId;
|
||||||
let query = this.albumRepository.createQueryBuilder('album');
|
let query = this.albumRepository.createQueryBuilder('album');
|
||||||
|
@ -132,35 +132,44 @@ export class AlbumRepository implements IAlbumRepository {
|
||||||
query = query
|
query = query
|
||||||
.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) => {
|
// .orWhere((qb) => {
|
||||||
const subQuery = qb
|
// const subQuery = qb
|
||||||
.subQuery()
|
// .subQuery()
|
||||||
.select('userAlbum.albumId')
|
// .select('userAlbum.albumId')
|
||||||
.from(UserAlbumEntity, 'userAlbum')
|
// .from(UserAlbumEntity, 'userAlbum')
|
||||||
.where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
|
// .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
|
||||||
.getQuery();
|
// .getQuery();
|
||||||
return `album.id IN ${subQuery}`;
|
// return `album.id IN ${subQuery}`;
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
return query.orderBy('album.createdAt', 'DESC').getMany();
|
// Get information of assets in albums
|
||||||
|
query = query
|
||||||
|
.leftJoinAndSelect('album.assets', 'assets')
|
||||||
|
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||||
|
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
|
||||||
|
const albums = await query.getMany();
|
||||||
|
|
||||||
|
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
||||||
|
|
||||||
|
return albums;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
||||||
const album = await this.albumRepository.findOne({
|
let query = this.albumRepository.createQueryBuilder('album');
|
||||||
where: { id: albumId },
|
|
||||||
relations: ['sharedUsers', 'sharedUsers.userInfo', 'assets', 'assets.assetInfo'],
|
const album = await query
|
||||||
});
|
.where('album.id = :albumId', { albumId })
|
||||||
|
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||||
|
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||||
|
.leftJoinAndSelect('album.assets', 'assets')
|
||||||
|
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||||
|
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
|
||||||
|
.getOne();
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: sort in query
|
|
||||||
const sortedSharedAsset = album.assets?.sort(
|
|
||||||
(a, b) => new Date(a.assetInfo.createdAt).valueOf() - new Date(b.assetInfo.createdAt).valueOf(),
|
|
||||||
);
|
|
||||||
|
|
||||||
album.assets = sortedSharedAsset;
|
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||||
|
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
|
@ -109,8 +110,11 @@ export class AssetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/thumbnail/:assetId')
|
@Get('/thumbnail/:assetId')
|
||||||
async getAssetThumbnail(@Param('assetId') assetId: string): Promise<any> {
|
async getAssetThumbnail(
|
||||||
return this.assetService.getAssetThumbnail(assetId);
|
@Param('assetId') assetId: string,
|
||||||
|
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
|
||||||
|
): Promise<any> {
|
||||||
|
return this.assetService.getAssetThumbnail(assetId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/allObjects')
|
@Get('/allObjects')
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto';
|
||||||
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||||
|
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
|
@ -187,7 +188,7 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAssetThumbnail(assetId: string) {
|
public async getAssetThumbnail(assetId: string, query: GetAssetThumbnailDto) {
|
||||||
let fileReadStream: ReadStream;
|
let fileReadStream: ReadStream;
|
||||||
|
|
||||||
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
|
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
|
||||||
|
@ -197,16 +198,25 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (asset.webpPath && asset.webpPath.length > 0) {
|
if (query.format == GetAssetThumbnailFormatEnum.JPEG) {
|
||||||
await fs.access(asset.webpPath, constants.R_OK | constants.W_OK);
|
|
||||||
fileReadStream = createReadStream(asset.webpPath);
|
|
||||||
} else {
|
|
||||||
if (!asset.resizePath) {
|
if (!asset.resizePath) {
|
||||||
throw new NotFoundException('resizePath not set');
|
throw new NotFoundException('resizePath not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
||||||
fileReadStream = createReadStream(asset.resizePath);
|
fileReadStream = createReadStream(asset.resizePath);
|
||||||
|
} else {
|
||||||
|
if (asset.webpPath && asset.webpPath.length > 0) {
|
||||||
|
await fs.access(asset.webpPath, constants.R_OK | constants.W_OK);
|
||||||
|
fileReadStream = createReadStream(asset.webpPath);
|
||||||
|
} else {
|
||||||
|
if (!asset.resizePath) {
|
||||||
|
throw new NotFoundException('resizePath not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
||||||
|
fileReadStream = createReadStream(asset.resizePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StreamableFile(fileReadStream);
|
return new StreamableFile(fileReadStream);
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export enum GetAssetThumbnailFormatEnum {
|
||||||
|
JPEG = 'JPEG',
|
||||||
|
WEBP = 'WEBP',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetAssetThumbnailDto {
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({
|
||||||
|
enum: GetAssetThumbnailFormatEnum,
|
||||||
|
default: GetAssetThumbnailFormatEnum.WEBP,
|
||||||
|
required: false,
|
||||||
|
enumName: 'ThumbnailFormat',
|
||||||
|
})
|
||||||
|
format = GetAssetThumbnailFormatEnum.WEBP;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
13
web/.eslintignore
Normal file
13
web/.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
|
@ -6,15 +6,15 @@ module.exports = {
|
||||||
ignorePatterns: ['*.cjs'],
|
ignorePatterns: ['*.cjs'],
|
||||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||||
settings: {
|
settings: {
|
||||||
'svelte3/typescript': () => require('typescript'),
|
'svelte3/typescript': () => require('typescript')
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2017: true,
|
es2017: true,
|
||||||
node: true,
|
node: true
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
2
web/.gitignore
vendored
2
web/.gitignore
vendored
|
@ -6,5 +6,3 @@ node_modules
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
.vercel
|
|
||||||
.output
|
|
||||||
|
|
13
web/.prettierignore
Normal file
13
web/.prettierignore
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "none",
|
||||||
"printWidth": 120,
|
"printWidth": 100
|
||||||
"semi": true
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
# default-template
|
|
||||||
|
|
||||||
## 0.0.2-next.0
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- [chore] upgrade cookie library ([#4592](https://github.com/sveltejs/kit/pull/4592))
|
|
2334
web/package-lock.json
generated
2334
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,22 +1,34 @@
|
||||||
{
|
{
|
||||||
"name": "web",
|
"name": "immich-web",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "svelte-kit dev --host 0.0.0.0",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
"build": "svelte-kit build",
|
"build": "vite build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "svelte-kit preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync",
|
"prepare": "svelte-kit sync",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "next",
|
"@sveltejs/adapter-auto": "next",
|
||||||
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
|
||||||
"@sveltejs/kit": "next",
|
"@sveltejs/kit": "next",
|
||||||
"@types/axios": "^0.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||||
|
"@typescript-eslint/parser": "^5.27.0",
|
||||||
|
"eslint": "^8.16.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"prettier-plugin-svelte": "^2.7.0",
|
||||||
|
"svelte": "^3.44.0",
|
||||||
|
"svelte-check": "^2.7.1",
|
||||||
|
"svelte-preprocess": "^4.10.6",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"vite": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-node": "next",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
"@types/fluent-ffmpeg": "^2.1.20",
|
"@types/fluent-ffmpeg": "^2.1.20",
|
||||||
|
@ -24,21 +36,9 @@
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/socket.io-client": "^3.0.0",
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"eslint": "^8.12.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
|
||||||
"postcss": "^8.4.13",
|
"postcss": "^8.4.13",
|
||||||
"prettier": "^2.5.1",
|
"tailwindcss": "^3.0.24"
|
||||||
"prettier-plugin-svelte": "^2.5.0",
|
|
||||||
"svelte": "^3.46.0",
|
|
||||||
"svelte-check": "^2.2.6",
|
|
||||||
"svelte-preprocess": "^4.10.1",
|
|
||||||
"tailwindcss": "^3.0.24",
|
|
||||||
"tslib": "^2.3.1",
|
|
||||||
"typescript": "~4.6.2"
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -957,6 +957,20 @@ export interface SmartInfoResponseDto {
|
||||||
*/
|
*/
|
||||||
'objects'?: Array<string> | null;
|
'objects'?: Array<string> | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ThumbnailFormat = {
|
||||||
|
Jpeg: 'JPEG',
|
||||||
|
Webp: 'WEBP'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -2069,10 +2083,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
|
* @param {ThumbnailFormat} [format]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAssetThumbnail: async (assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getAssetThumbnail: async (assetId: string, format?: ThumbnailFormat, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'assetId' is not null or undefined
|
// verify required parameter 'assetId' is not null or undefined
|
||||||
assertParamExists('getAssetThumbnail', 'assetId', assetId)
|
assertParamExists('getAssetThumbnail', 'assetId', assetId)
|
||||||
const localVarPath = `/asset/thumbnail/{assetId}`
|
const localVarPath = `/asset/thumbnail/{assetId}`
|
||||||
|
@ -2092,6 +2107,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
if (format !== undefined) {
|
||||||
|
localVarQueryParameter['format'] = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
@ -2424,11 +2443,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
|
* @param {ThumbnailFormat} [format]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getAssetThumbnail(assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
async getAssetThumbnail(assetId: string, format?: ThumbnailFormat, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetThumbnail(assetId, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetThumbnail(assetId, format, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -2564,11 +2584,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
|
* @param {ThumbnailFormat} [format]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getAssetThumbnail(assetId: string, options?: any): AxiosPromise<object> {
|
getAssetThumbnail(assetId: string, format?: ThumbnailFormat, options?: any): AxiosPromise<object> {
|
||||||
return localVarFp.getAssetThumbnail(assetId, options).then((request) => request(axios, basePath));
|
return localVarFp.getAssetThumbnail(assetId, format, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -2709,12 +2730,13 @@ export class AssetApi extends BaseAPI {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
|
* @param {ThumbnailFormat} [format]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getAssetThumbnail(assetId: string, options?: AxiosRequestConfig) {
|
public getAssetThumbnail(assetId: string, format?: ThumbnailFormat, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getAssetThumbnail(assetId, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getAssetThumbnail(assetId, format, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
<head>
|
||||||
<link rel="icon" href="%svelte.assets%/favicon.png" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
%svelte.head%
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</head>
|
%sveltekit.head%
|
||||||
<body>
|
</head>
|
||||||
<div>%svelte.body%</div>
|
|
||||||
</body>
|
<body>
|
||||||
</html>
|
<div>%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
57
web/src/lib/components/album/album-card.svelte
Normal file
57
web/src/lib/components/album/album-card.svelte
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlbumResponseDto, api, ThumbnailFormat } from '@api';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
export let album: AlbumResponseDto;
|
||||||
|
|
||||||
|
let imageData: string = '/no-thumbnail.png';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const loadImageData = async (thubmnailId: string | null) => {
|
||||||
|
if (thubmnailId == null) {
|
||||||
|
return '/no-thumbnail.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.assetApi.getAssetThumbnail(thubmnailId!, ThumbnailFormat.Jpeg, { responseType: 'blob' });
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
imageData = URL.createObjectURL(data);
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-[339px] w-[275px] hover:cursor-pointer mt-4" on:click={() => dispatch('click', album)}>
|
||||||
|
<div class={`h-[275px] w-[275px]`}>
|
||||||
|
{#await loadImageData(album.albumThumbnailAssetId)}
|
||||||
|
<div class={`bg-immich-primary/10 w-full h-full flex place-items-center place-content-center rounded-xl`}>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
{:then imageData}
|
||||||
|
<img
|
||||||
|
in:fade={{ duration: 250 }}
|
||||||
|
src={imageData}
|
||||||
|
alt={album.id}
|
||||||
|
class={`object-cover w-full h-full transition-all z-0 rounded-xl duration-300 hover:translate-x-2 hover:-translate-y-2 hover:shadow-[-8px_8px_0px_0_#FFB800]`}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm font-medium text-gray-800">
|
||||||
|
{album.albumName}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<span class="text-xs flex gap-2">
|
||||||
|
<p>{album.assets.length} items</p>
|
||||||
|
|
||||||
|
{#if album.shared}
|
||||||
|
<p>·</p>
|
||||||
|
<p>Shared</p>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
101
web/src/lib/components/album/album-viewer.svelte
Normal file
101
web/src/lib/components/album/album-viewer.svelte
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AlbumResponseDto, ThumbnailFormat } from '@api';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||||
|
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
|
||||||
|
import CircleAvatar from '../shared/circle-avatar.svelte';
|
||||||
|
import ImmichThumbnail from '../shared/immich-thumbnail.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let album: AlbumResponseDto;
|
||||||
|
let viewWidth: number;
|
||||||
|
let thumbnailSize: number = 300;
|
||||||
|
let border = '';
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (album.assets.length < 6) {
|
||||||
|
thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length);
|
||||||
|
} else {
|
||||||
|
thumbnailSize = Math.floor(viewWidth / 6 - 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDateRange = () => {
|
||||||
|
const startDate = new Date(album.assets[0].createdAt);
|
||||||
|
const endDate = new Date(album.assets[album.assets.length - 1].createdAt);
|
||||||
|
|
||||||
|
const startDateString = startDate.toLocaleDateString('us-EN', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
const endDateString = endDate.toLocaleDateString('us-EN', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
return `${startDateString} - ${endDateString}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.onscroll = (event: Event) => {
|
||||||
|
if (window.pageYOffset > 80) {
|
||||||
|
border = 'border border-gray-200 bg-gray-50';
|
||||||
|
} else {
|
||||||
|
border = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="w-screen h-screen bg-immich-bg">
|
||||||
|
<div class="fixed top-0 w-full bg-immich-bg z-[100]">
|
||||||
|
<div class={`flex justify-between rounded-lg ${border} p-2 mx-2 mt-2 transition-all`}>
|
||||||
|
<a sveltekit:prefetch href="/albums" title="Go Back">
|
||||||
|
<button
|
||||||
|
id="immich-circle-icon-button"
|
||||||
|
class={`rounded-full p-3 flex place-items-center place-content-center text-gray-600 transition-all hover:bg-gray-200`}
|
||||||
|
>
|
||||||
|
<ArrowLeft size="24" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<div class="right-button-group" title="Add Photos">
|
||||||
|
<button
|
||||||
|
id="immich-circle-icon-button"
|
||||||
|
class={`rounded-full p-3 flex place-items-center place-content-center text-gray-600 transition-all hover:bg-gray-200`}
|
||||||
|
on:click={() => dispatch('click')}
|
||||||
|
>
|
||||||
|
<FileImagePlusOutline size="24" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="m-6 py-[72px] px-[160px]">
|
||||||
|
<p class="text-6xl text-immich-primary">
|
||||||
|
{album.albumName}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="my-4 text-sm text-gray-500">{getDateRange()}</p>
|
||||||
|
|
||||||
|
{#if album.sharedUsers.length > 0}
|
||||||
|
<div class="mb-4">
|
||||||
|
{#each album.sharedUsers as user}
|
||||||
|
<span class="mr-1">
|
||||||
|
<CircleAvatar {user} />
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-1 w-full" bind:clientWidth={viewWidth}>
|
||||||
|
{#each album.assets as asset}
|
||||||
|
{#if album.assets.length < 7}
|
||||||
|
<ImmichThumbnail {asset} {thumbnailSize} format={ThumbnailFormat.Jpeg} />
|
||||||
|
{:else}
|
||||||
|
<ImmichThumbnail {asset} {thumbnailSize} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
|
@ -6,7 +6,7 @@
|
||||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||||
import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
|
import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
|
||||||
import CircleIconButton from '../shared/circle_icon_button.svelte';
|
import CircleIconButton from '../shared/circle-icon-button.svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,12 @@
|
||||||
import { flattenAssetGroupByDate } from '$lib/stores/assets';
|
import { flattenAssetGroupByDate } from '$lib/stores/assets';
|
||||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||||
import { AssetType } from '../../models/immich-asset';
|
|
||||||
import PhotoViewer from './photo-viewer.svelte';
|
import PhotoViewer from './photo-viewer.svelte';
|
||||||
import DetailPanel from './detail-panel.svelte';
|
import DetailPanel from './detail-panel.svelte';
|
||||||
import { session } from '$app/stores';
|
import { session } from '$app/stores';
|
||||||
import { downloadAssets } from '$lib/stores/download';
|
import { downloadAssets } from '$lib/stores/download';
|
||||||
import VideoViewer from './video-viewer.svelte';
|
import VideoViewer from './video-viewer.svelte';
|
||||||
import { api, AssetResponseDto } from '@api';
|
import { api, AssetResponseDto, AssetTypeEnum } from '@api';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -191,7 +190,7 @@
|
||||||
<div class="row-start-1 row-span-full col-start-1 col-span-4">
|
<div class="row-start-1 row-span-full col-start-1 col-span-4">
|
||||||
{#key selectedIndex}
|
{#key selectedIndex}
|
||||||
{#if viewAssetId && viewDeviceId}
|
{#if viewAssetId && viewDeviceId}
|
||||||
{#if selectedAsset.type == AssetType.IMAGE}
|
{#if selectedAsset.type == AssetTypeEnum.Image}
|
||||||
<PhotoViewer assetId={viewAssetId} deviceId={viewDeviceId} on:close={closeViewer} />
|
<PhotoViewer assetId={viewAssetId} deviceId={viewDeviceId} on:close={closeViewer} />
|
||||||
{:else}
|
{:else}
|
||||||
<VideoViewer assetId={viewAssetId} on:close={closeViewer} />
|
<VideoViewer assetId={viewAssetId} on:close={closeViewer} />
|
||||||
|
|
35
web/src/lib/components/shared/circle-avatar.svelte
Normal file
35
web/src/lib/components/shared/circle-avatar.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { api, UserResponseDto } from '@api';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserAvatar = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.userApi.getProfileImage(user.id, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
return URL.createObjectURL(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return '/favicon.png';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await getUserAvatar()}
|
||||||
|
<div class="w-12 h-12 rounded-full bg-immich-primary/25" />
|
||||||
|
{:then data}
|
||||||
|
<img
|
||||||
|
src={data}
|
||||||
|
alt="profile-img"
|
||||||
|
class="inline rounded-full w-12 h-12 object-cover border shadow-md"
|
||||||
|
title={user.email}
|
||||||
|
/>
|
||||||
|
{/await}
|
|
@ -1,15 +0,0 @@
|
||||||
export function clickOutside(node: Node) {
|
|
||||||
const handleClick = (event: any) => {
|
|
||||||
if (!node.contains(event.target)) {
|
|
||||||
node.dispatchEvent(new CustomEvent("outclick"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("click", handleClick, true);
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
document.removeEventListener("click", handleClick, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { clickOutside } from './click-outside';
|
import { clickOutside } from '../../utils/click-outside';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
out:fade={{ duration: 100 }}
|
out:fade={{ duration: 100 }}
|
||||||
class="absolute w-full h-full bg-black/40 z-[100] flex place-items-center place-content-center "
|
class="absolute w-full h-full bg-black/40 z-[100] flex place-items-center place-content-center "
|
||||||
>
|
>
|
||||||
<div class="z-[9999]" use:clickOutside on:outclick={() => dispatch('clickOutside')}>
|
<div class="z-[9999]" use:clickOutside on:out-click={() => dispatch('clickOutside')}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AssetType } from '../../models/immich-asset';
|
|
||||||
import { session } from '$app/stores';
|
import { session } from '$app/stores';
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||||
import { fade, fly } from 'svelte/transition';
|
import { fade, fly } from 'svelte/transition';
|
||||||
|
@ -7,13 +6,15 @@
|
||||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||||
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
||||||
import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
|
import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
|
||||||
import LoadingSpinner from '../shared/loading-spinner.svelte';
|
import LoadingSpinner from './loading-spinner.svelte';
|
||||||
import { api, AssetResponseDto } from '@api';
|
import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let groupIndex: number;
|
export let groupIndex = 0;
|
||||||
|
export let thumbnailSize: number | undefined = undefined;
|
||||||
|
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
|
||||||
|
|
||||||
let imageData: string;
|
let imageData: string;
|
||||||
let videoData: string;
|
let videoData: string;
|
||||||
|
@ -29,7 +30,9 @@
|
||||||
|
|
||||||
const loadImageData = async () => {
|
const loadImageData = async () => {
|
||||||
if ($session.user) {
|
if ($session.user) {
|
||||||
const { data } = await api.assetApi.getAssetThumbnail(asset.id, { responseType: 'blob' });
|
const { data } = await api.assetApi.getAssetThumbnail(asset.id, format, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
if (data instanceof Blob) {
|
if (data instanceof Blob) {
|
||||||
imageData = URL.createObjectURL(data);
|
imageData = URL.createObjectURL(data);
|
||||||
return imageData;
|
return imageData;
|
||||||
|
@ -42,9 +45,15 @@
|
||||||
|
|
||||||
if ($session.user) {
|
if ($session.user) {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.assetApi.serveFile(asset.deviceAssetId, asset.deviceId, false, true, {
|
const { data } = await api.assetApi.serveFile(
|
||||||
responseType: 'blob',
|
asset.deviceAssetId,
|
||||||
});
|
asset.deviceId,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
responseType: 'blob'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!(data instanceof Blob)) {
|
if (!(data instanceof Blob)) {
|
||||||
return;
|
return;
|
||||||
|
@ -109,6 +118,10 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSize = () => {
|
const getSize = () => {
|
||||||
|
if (thumbnailSize) {
|
||||||
|
return `w-[${thumbnailSize}px] h-[${thumbnailSize}px]`;
|
||||||
|
}
|
||||||
|
|
||||||
if (asset.exifInfo?.orientation === 'Rotate 90 CW') {
|
if (asset.exifInfo?.orientation === 'Rotate 90 CW') {
|
||||||
return 'w-[176px] h-[235px]';
|
return 'w-[176px] h-[235px]';
|
||||||
} else if (asset.exifInfo?.orientation === 'Horizontal (normal)') {
|
} else if (asset.exifInfo?.orientation === 'Horizontal (normal)') {
|
||||||
|
@ -135,6 +148,8 @@
|
||||||
|
|
||||||
<IntersectionObserver once={true} let:intersecting>
|
<IntersectionObserver once={true} let:intersecting>
|
||||||
<div
|
<div
|
||||||
|
style:width={`${thumbnailSize}px`}
|
||||||
|
style:height={`${thumbnailSize}px`}
|
||||||
class={`bg-gray-100 relative hover:cursor-pointer ${getSize()}`}
|
class={`bg-gray-100 relative hover:cursor-pointer ${getSize()}`}
|
||||||
on:mouseenter={handleMouseOverThumbnail}
|
on:mouseenter={handleMouseOverThumbnail}
|
||||||
on:mouseleave={handleMouseLeaveThumbnail}
|
on:mouseleave={handleMouseLeaveThumbnail}
|
||||||
|
@ -156,8 +171,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Playback and info -->
|
<!-- Playback and info -->
|
||||||
{#if asset.type === AssetType.VIDEO}
|
{#if asset.type === AssetTypeEnum.Video}
|
||||||
<div class="absolute right-2 top-2 text-white text-xs font-medium flex gap-1 place-items-center z-10">
|
<div
|
||||||
|
class="absolute right-2 top-2 text-white text-xs font-medium flex gap-1 place-items-center z-10"
|
||||||
|
>
|
||||||
{#if isThumbnailVideoPlaying}
|
{#if isThumbnailVideoPlaying}
|
||||||
<span in:fly={{ x: -25, duration: 500 }}>
|
<span in:fly={{ x: -25, duration: 500 }}>
|
||||||
{videoProgress}
|
{videoProgress}
|
||||||
|
@ -189,9 +206,17 @@
|
||||||
<!-- Thumbnail -->
|
<!-- Thumbnail -->
|
||||||
{#if intersecting}
|
{#if intersecting}
|
||||||
{#await loadImageData()}
|
{#await loadImageData()}
|
||||||
<div class={`bg-immich-primary/10 ${getSize()} flex place-items-center place-content-center`}>...</div>
|
<div
|
||||||
|
style:width={`${thumbnailSize}px`}
|
||||||
|
style:height={`${thumbnailSize}px`}
|
||||||
|
class={`bg-immich-primary/10 ${getSize()} flex place-items-center place-content-center`}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
{:then imageData}
|
{:then imageData}
|
||||||
<img
|
<img
|
||||||
|
style:width={`${thumbnailSize}px`}
|
||||||
|
style:height={`${thumbnailSize}px`}
|
||||||
in:fade={{ duration: 250 }}
|
in:fade={{ duration: 250 }}
|
||||||
src={imageData}
|
src={imageData}
|
||||||
alt={asset.id}
|
alt={asset.id}
|
||||||
|
@ -201,9 +226,17 @@
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if mouseOver && asset.type === AssetType.VIDEO}
|
{#if mouseOver && asset.type === AssetTypeEnum.Video}
|
||||||
<div class="absolute w-full h-full top-0" on:mouseenter={loadVideoData}>
|
<div class="absolute w-full h-full top-0" on:mouseenter={loadVideoData}>
|
||||||
<video muted autoplay preload="none" class="h-full object-cover" width="250px" bind:this={videoPlayerNode}>
|
<video
|
||||||
|
muted
|
||||||
|
autoplay
|
||||||
|
preload="none"
|
||||||
|
class="h-full object-cover"
|
||||||
|
width="250px"
|
||||||
|
style:width={`${thumbnailSize}px`}
|
||||||
|
bind:this={videoPlayerNode}
|
||||||
|
>
|
||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
|
@ -7,7 +7,7 @@
|
||||||
import { fade, fly, slide } from 'svelte/transition';
|
import { fade, fly, slide } from 'svelte/transition';
|
||||||
import { serverEndpoint } from '../../constants';
|
import { serverEndpoint } from '../../constants';
|
||||||
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
|
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
|
||||||
import { clickOutside } from './click-outside';
|
import { clickOutside } from '../../utils/click-outside';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
|
|
||||||
export let user: ImmichUser;
|
export let user: ImmichUser;
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
|
|
||||||
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
|
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
|
||||||
<div class="flex border-b place-items-center px-6 py-2 ">
|
<div class="flex border-b place-items-center px-6 py-2 ">
|
||||||
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
<a sveltekit:prefetch class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
|
||||||
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
|
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
|
||||||
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
|
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
|
||||||
</a>
|
</a>
|
||||||
|
@ -76,12 +76,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if user.isAdmin}
|
{#if user.isAdmin}
|
||||||
<button
|
<a sveltekit:prefetch href={`admin`}>
|
||||||
class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium ${
|
<button
|
||||||
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium ${
|
||||||
}`}
|
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
||||||
on:click={navigateToAdmin}>Administration</button
|
}`}>Administration</button
|
||||||
>
|
>
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -125,7 +126,7 @@
|
||||||
id="account-info-panel"
|
id="account-info-panel"
|
||||||
class="absolute right-[25px] top-[75px] bg-white shadow-lg rounded-2xl w-[360px] text-center"
|
class="absolute right-[25px] top-[75px] bg-white shadow-lg rounded-2xl w-[360px] text-center"
|
||||||
use:clickOutside
|
use:clickOutside
|
||||||
on:outclick={() => (shouldShowAccountInfoPanel = false)}
|
on:out-click={() => (shouldShowAccountInfoPanel = false)}
|
||||||
>
|
>
|
||||||
<div class="flex place-items-center place-content-center mt-6">
|
<div class="flex place-items-center place-content-center mt-6">
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -5,13 +5,16 @@
|
||||||
export let isSelected: boolean;
|
export let isSelected: boolean;
|
||||||
|
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type { AdminSideBarSelection, AppSideBarSelection } from '../../models/admin-sidebar-selection';
|
import type {
|
||||||
|
AdminSideBarSelection,
|
||||||
|
AppSideBarSelection
|
||||||
|
} from '../../../models/admin-sidebar-selection';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const onButtonClicked = () => {
|
const onButtonClicked = () => {
|
||||||
dispatch('selected', {
|
dispatch('selected', {
|
||||||
actionType,
|
actionType
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
65
web/src/lib/components/shared/side-bar/side-bar.svelte
Normal file
65
web/src/lib/components/shared/side-bar/side-bar.svelte
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
|
||||||
|
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||||
|
import SideBarButton from './side-bar-button.svelte';
|
||||||
|
import StatusBox from '../status-box.svelte';
|
||||||
|
|
||||||
|
let selectedAction: AppSideBarSelection;
|
||||||
|
|
||||||
|
const onSidebarButtonClicked = (buttonType: CustomEvent) => {
|
||||||
|
selectedAction = buttonType.detail['actionType'] as AppSideBarSelection;
|
||||||
|
|
||||||
|
if (selectedAction == AppSideBarSelection.PHOTOS) {
|
||||||
|
if ($page.routeId != 'photos') {
|
||||||
|
goto('/photos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAction == AppSideBarSelection.ALBUMS) {
|
||||||
|
if ($page.routeId != 'albums') {
|
||||||
|
goto('/albums');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if ($page.routeId == 'albums') {
|
||||||
|
selectedAction = AppSideBarSelection.ALBUMS;
|
||||||
|
} else if ($page.routeId == 'photos') {
|
||||||
|
selectedAction = AppSideBarSelection.PHOTOS;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
|
||||||
|
<a sveltekit:prefetch href={$page.routeId != 'photos' ? `/photos` : null}>
|
||||||
|
<SideBarButton
|
||||||
|
title="Photos"
|
||||||
|
logo={ImageOutline}
|
||||||
|
actionType={AppSideBarSelection.PHOTOS}
|
||||||
|
isSelected={selectedAction === AppSideBarSelection.PHOTOS}
|
||||||
|
/></a
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="text-xs ml-5">
|
||||||
|
<p>LIBRARY</p>
|
||||||
|
</div>
|
||||||
|
<a sveltekit:prefetch href={$page.routeId != 'albums' ? `/albums` : null}>
|
||||||
|
<SideBarButton
|
||||||
|
title="Albums"
|
||||||
|
logo={ImageAlbum}
|
||||||
|
actionType={AppSideBarSelection.ALBUMS}
|
||||||
|
isSelected={selectedAction === AppSideBarSelection.ALBUMS}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<!-- Status Box -->
|
||||||
|
|
||||||
|
<div class="mb-6 mt-auto">
|
||||||
|
<StatusBox />
|
||||||
|
</div>
|
||||||
|
</section>
|
|
@ -1,9 +1,9 @@
|
||||||
export enum AdminSideBarSelection {
|
export enum AdminSideBarSelection {
|
||||||
USER_MANAGEMENT = "User management",
|
USER_MANAGEMENT = 'User management',
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppSideBarSelection {
|
export enum AppSideBarSelection {
|
||||||
PHOTOS = "Photos",
|
PHOTOS = 'Photos',
|
||||||
EXPLORE = "Explore",
|
EXPLORE = 'Explore',
|
||||||
}
|
ALBUMS = 'Albums',
|
||||||
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
export enum AssetType {
|
|
||||||
IMAGE = 'IMAGE',
|
|
||||||
VIDEO = 'VIDEO',
|
|
||||||
AUDIO = 'AUDIO',
|
|
||||||
OTHER = 'OTHER',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImmichExif = {
|
|
||||||
id: string;
|
|
||||||
assetId: string;
|
|
||||||
make: string;
|
|
||||||
model: string;
|
|
||||||
imageName: string;
|
|
||||||
exifImageWidth: number;
|
|
||||||
exifImageHeight: number;
|
|
||||||
fileSizeInByte: number;
|
|
||||||
orientation: string;
|
|
||||||
dateTimeOriginal: Date;
|
|
||||||
modifyDate: Date;
|
|
||||||
lensModel: string;
|
|
||||||
fNumber: number;
|
|
||||||
focalLength: number;
|
|
||||||
iso: number;
|
|
||||||
exposureTime: number;
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
country: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImmichAssetSmartInfo = {
|
|
||||||
id: string;
|
|
||||||
assetId: string;
|
|
||||||
tags: string[];
|
|
||||||
objects: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImmichAsset = {
|
|
||||||
id: string;
|
|
||||||
deviceAssetId: string;
|
|
||||||
userId: string;
|
|
||||||
deviceId: string;
|
|
||||||
type: AssetType;
|
|
||||||
originalPath: string;
|
|
||||||
resizePath: string;
|
|
||||||
createdAt: string;
|
|
||||||
modifiedAt: string;
|
|
||||||
isFavorite: boolean;
|
|
||||||
mimeType: string;
|
|
||||||
duration: string;
|
|
||||||
exifInfo?: ImmichExif;
|
|
||||||
smartInfo?: ImmichAssetSmartInfo;
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { Socket, io } from 'socket.io-client';
|
import { Socket, io } from 'socket.io-client';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { serverEndpoint } from '../constants';
|
import { serverEndpoint } from '../constants';
|
||||||
import type { ImmichAsset } from '../models/immich-asset';
|
|
||||||
import { assets } from './assets';
|
|
||||||
|
|
||||||
let websocket: Socket;
|
let websocket: Socket;
|
||||||
|
|
||||||
|
@ -28,10 +26,7 @@ export const openWebsocketConnection = (accessToken: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const listenToEvent = (socket: Socket) => {
|
const listenToEvent = (socket: Socket) => {
|
||||||
socket.on('on_upload_success', (data) => {
|
socket.on('on_upload_success', (data) => {});
|
||||||
const newUploadedAsset: ImmichAsset = JSON.parse(data);
|
|
||||||
// assets.update((assets) => [...assets, newUploadedAsset]);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (e) => {
|
socket.on('error', (e) => {
|
||||||
console.log('Websocket Error', e);
|
console.log('Websocket Error', e);
|
||||||
|
|
15
web/src/lib/utils/click-outside.ts
Normal file
15
web/src/lib/utils/click-outside.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export function clickOutside(node: Node) {
|
||||||
|
const handleClick = (event: any) => {
|
||||||
|
if (!node.contains(event.target)) {
|
||||||
|
node.dispatchEvent(new CustomEvent('out-click'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClick, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('click', handleClick, true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
import { blur } from 'svelte/transition';
|
import { blur, fade, slide } from 'svelte/transition';
|
||||||
|
|
||||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||||
import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
|
import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{#key url}
|
{#key url}
|
||||||
<div transition:blur={{ duration: 250 }}>
|
<div in:fade={{ duration: 100 }}>
|
||||||
<slot />
|
<slot />
|
||||||
<DownloadPanel />
|
<DownloadPanel />
|
||||||
<UploadPanel />
|
<UploadPanel />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
|
|
||||||
export const post: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
|
|
||||||
const email = form.get('email');
|
const email = form.get('email');
|
||||||
|
@ -13,22 +13,22 @@ export const post: RequestHandler = async ({ request }) => {
|
||||||
email: String(email),
|
email: String(email),
|
||||||
password: String(password),
|
password: String(password),
|
||||||
firstName: String(firstName),
|
firstName: String(firstName),
|
||||||
lastName: String(lastName),
|
lastName: String(lastName)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 201) {
|
if (status === 201) {
|
||||||
return {
|
return {
|
||||||
status: 201,
|
status: 201,
|
||||||
body: {
|
body: {
|
||||||
success: 'Succesfully create user account',
|
success: 'Succesfully create user account'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
error: 'Error create user account',
|
error: 'Error create user account'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
import type { ImmichUser } from '$lib/models/immich-user';
|
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/side-bar-button.svelte';
|
import SideBarButton from '$lib/components/shared/side-bar/side-bar-button.svelte';
|
||||||
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
|
||||||
import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
|
||||||
import UserManagement from '$lib/components/admin/user-management.svelte';
|
import UserManagement from '$lib/components/admin/user-management.svelte';
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Immich - Administration</title>
|
<title>Administration - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<NavigationBar {user} />
|
<NavigationBar {user} />
|
||||||
|
|
49
web/src/routes/albums/[albumId].svelte
Normal file
49
web/src/routes/albums/[albumId].svelte
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
import { AlbumResponseDto, api } from '@api';
|
||||||
|
|
||||||
|
export const load: Load = async ({ session, params }) => {
|
||||||
|
if (!session.user) {
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/auth/login'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const albumId = params['albumId'];
|
||||||
|
|
||||||
|
let album: AlbumResponseDto;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await api.albumApi.getAlbumInfo(albumId);
|
||||||
|
album = data;
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/albums'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
props: {
|
||||||
|
album: album
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import AlbumViewer from '$lib/components/album/album-viewer.svelte';
|
||||||
|
|
||||||
|
export let album: AlbumResponseDto;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{album.albumName} - Immich</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<AlbumViewer {album} />
|
94
web/src/routes/albums/index.svelte
Normal file
94
web/src/routes/albums/index.svelte
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||||
|
|
||||||
|
import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
|
||||||
|
import { ImmichUser } from '$lib/models/immich-user';
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
import SideBar from '$lib/components/shared/side-bar/side-bar.svelte';
|
||||||
|
import { AlbumResponseDto, api } from '@api';
|
||||||
|
|
||||||
|
export const load: Load = async ({ session }) => {
|
||||||
|
if (!session.user) {
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/auth/login'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let allAlbums: AlbumResponseDto[] = [];
|
||||||
|
try {
|
||||||
|
const { data } = await api.albumApi.getAllAlbums();
|
||||||
|
allAlbums = data;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error [getAllAlbums] ', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
props: {
|
||||||
|
user: session.user,
|
||||||
|
allAlbums: allAlbums
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import AlbumCard from '$lib/components/album/album-card.svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
export let user: ImmichUser;
|
||||||
|
export let allAlbums: AlbumResponseDto[];
|
||||||
|
|
||||||
|
const showAlbum = (event: CustomEvent) => {
|
||||||
|
goto('/albums/' + event.detail.id);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Albums - Immich</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<NavigationBar {user} on:uploadClicked={() => {}} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||||
|
<SideBar />
|
||||||
|
|
||||||
|
<!-- Main Section -->
|
||||||
|
|
||||||
|
<section class="overflow-y-auto relative">
|
||||||
|
<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
|
||||||
|
<div class="px-4 flex justify-between place-items-center">
|
||||||
|
<div>
|
||||||
|
<p>Albums</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<PlusBoxOutline size="18" />
|
||||||
|
</span>
|
||||||
|
<p>Create album</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Album Card -->
|
||||||
|
<div class="flex flex-wrap gap-8">
|
||||||
|
{#each allAlbums as album}
|
||||||
|
<a sveltekit:prefetch href={`albums/${album.id}`}> <AlbumCard {album} /></a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
|
@ -57,7 +57,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Immich - Change Password</title>
|
<title>Change Password - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
|
|
||||||
export const post: RequestHandler = async ({ request, locals }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
if (!locals.user) {
|
if (!locals.user) {
|
||||||
return {
|
return {
|
||||||
status: 401,
|
status: 401,
|
||||||
body: {
|
body: {
|
||||||
error: 'Unauthorized',
|
error: 'Unauthorized'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,22 +17,22 @@ export const post: RequestHandler = async ({ request, locals }) => {
|
||||||
const { status } = await api.userApi.updateUser({
|
const { status } = await api.userApi.updateUser({
|
||||||
id: locals.user.id,
|
id: locals.user.id,
|
||||||
password: String(password),
|
password: String(password),
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
success: 'Succesfully change password',
|
success: 'Succesfully change password'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
error: 'Error change password',
|
error: 'Error change password'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Immich - Login</title>
|
<title>Login - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
|
|
||||||
export const post: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
|
|
||||||
const email = form.get('email');
|
const email = form.get('email');
|
||||||
|
@ -11,7 +11,7 @@ export const post: RequestHandler = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const { data: authUser } = await api.authenticationApi.login({
|
const { data: authUser } = await api.authenticationApi.login({
|
||||||
email: String(email),
|
email: String(email),
|
||||||
password: String(password),
|
password: String(password)
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -24,9 +24,9 @@ export const post: RequestHandler = async ({ request }) => {
|
||||||
lastName: authUser.lastName,
|
lastName: authUser.lastName,
|
||||||
isAdmin: authUser.isAdmin,
|
isAdmin: authUser.isAdmin,
|
||||||
email: authUser.userEmail,
|
email: authUser.userEmail,
|
||||||
shouldChangePassword: authUser.shouldChangePassword,
|
shouldChangePassword: authUser.shouldChangePassword
|
||||||
},
|
},
|
||||||
success: 'success',
|
success: 'success'
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Set-Cookie': cookie.serialize(
|
'Set-Cookie': cookie.serialize(
|
||||||
|
@ -37,23 +37,23 @@ export const post: RequestHandler = async ({ request }) => {
|
||||||
firstName: authUser.firstName,
|
firstName: authUser.firstName,
|
||||||
lastName: authUser.lastName,
|
lastName: authUser.lastName,
|
||||||
isAdmin: authUser.isAdmin,
|
isAdmin: authUser.isAdmin,
|
||||||
email: authUser.userEmail,
|
email: authUser.userEmail
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
maxAge: 60 * 60 * 24 * 30,
|
maxAge: 60 * 60 * 24 * 30
|
||||||
},
|
}
|
||||||
),
|
)
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
error: 'Incorrect email or password',
|
error: 'Incorrect email or password'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const post: RequestHandler = async () => {
|
export const POST: RequestHandler = async () => {
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
'Set-Cookie': 'session=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT',
|
'Set-Cookie': 'session=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
ok: true,
|
ok: true
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Immich - Admin Registration</title>
|
<title>Admin Registration - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
|
|
||||||
export const post: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
|
|
||||||
const email = form.get('email');
|
const email = form.get('email');
|
||||||
|
@ -13,22 +13,22 @@ export const post: RequestHandler = async ({ request }) => {
|
||||||
email: String(email),
|
email: String(email),
|
||||||
password: String(password),
|
password: String(password),
|
||||||
firstName: String(firstName),
|
firstName: String(firstName),
|
||||||
lastName: String(lastName),
|
lastName: String(lastName)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status === 201) {
|
if (status === 201) {
|
||||||
return {
|
return {
|
||||||
status: 201,
|
status: 201,
|
||||||
body: {
|
body: {
|
||||||
success: 'Succesfully create admin account',
|
success: 'Succesfully create admin account'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
error: 'Error create admin account',
|
error: 'Error create admin account'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Immich - Welcome 🎉</title>
|
<title>Welcome 🎉 - Immich</title>
|
||||||
<meta name="description" content="Immich Web Interface" />
|
<meta name="description" content="Immich Web Interface" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
if (!session.user) {
|
if (!session.user) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/auth/login',
|
redirect: '/auth/login'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
props: {
|
props: {
|
||||||
user: session.user,
|
user: session.user
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -27,26 +27,19 @@
|
||||||
import type { ImmichUser } from '$lib/models/immich-user';
|
import type { ImmichUser } from '$lib/models/immich-user';
|
||||||
|
|
||||||
import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared/navigation-bar.svelte';
|
||||||
import SideBarButton from '$lib/components/shared/side-bar-button.svelte';
|
|
||||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||||
|
|
||||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
|
||||||
import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { session } from '$app/stores';
|
import { session } from '$app/stores';
|
||||||
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
|
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
|
||||||
import ImmichThumbnail from '$lib/components/asset-viewer/immich-thumbnail.svelte';
|
import ImmichThumbnail from '$lib/components/shared/immich-thumbnail.svelte';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||||
import StatusBox from '$lib/components/shared/status-box.svelte';
|
|
||||||
import { fileUploader } from '$lib/utils/file-uploader';
|
import { fileUploader } from '$lib/utils/file-uploader';
|
||||||
import { AssetResponseDto } from '@api';
|
import { AssetResponseDto } from '@api';
|
||||||
|
import SideBar from '$lib/components/shared/side-bar/side-bar.svelte';
|
||||||
|
|
||||||
export let user: ImmichUser;
|
export let user: ImmichUser;
|
||||||
|
|
||||||
let selectedAction: AppSideBarSelection;
|
|
||||||
|
|
||||||
let selectedGroupThumbnail: number | null;
|
let selectedGroupThumbnail: number | null;
|
||||||
let isMouseOverGroup: boolean;
|
let isMouseOverGroup: boolean;
|
||||||
$: if (isMouseOverGroup == false) {
|
$: if (isMouseOverGroup == false) {
|
||||||
|
@ -57,14 +50,6 @@
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
let currentSelectedAsset: AssetResponseDto;
|
let currentSelectedAsset: AssetResponseDto;
|
||||||
|
|
||||||
const onButtonClicked = (buttonType: CustomEvent) => {
|
|
||||||
selectedAction = buttonType.detail['actionType'] as AppSideBarSelection;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
selectedAction = AppSideBarSelection.PHOTOS;
|
|
||||||
});
|
|
||||||
|
|
||||||
const thumbnailMouseEventHandler = (event: CustomEvent) => {
|
const thumbnailMouseEventHandler = (event: CustomEvent) => {
|
||||||
const { selectedGroupIndex }: { selectedGroupIndex: number } = event.detail;
|
const { selectedGroupIndex }: { selectedGroupIndex: number } = event.detail;
|
||||||
|
|
||||||
|
@ -92,7 +77,7 @@
|
||||||
const files = Array.from<File>(e.target.files);
|
const files = Array.from<File>(e.target.files);
|
||||||
|
|
||||||
const acceptedFile = files.filter(
|
const acceptedFile = files.filter(
|
||||||
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image',
|
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const asset of acceptedFile) {
|
for (const asset of acceptedFile) {
|
||||||
|
@ -109,7 +94,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Immich - Photos</title>
|
<title>Photos - Immich</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
@ -117,22 +102,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||||
<!-- Sidebar -->
|
<SideBar />
|
||||||
<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
|
|
||||||
<SideBarButton
|
|
||||||
title="Photos"
|
|
||||||
logo={ImageOutline}
|
|
||||||
actionType={AppSideBarSelection.PHOTOS}
|
|
||||||
isSelected={selectedAction === AppSideBarSelection.PHOTOS}
|
|
||||||
on:selected={onButtonClicked}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Status Box -->
|
|
||||||
|
|
||||||
<div class="mb-6 mt-auto">
|
|
||||||
<StatusBox />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Main Section -->
|
<!-- Main Section -->
|
||||||
<section class="overflow-y-auto relative">
|
<section class="overflow-y-auto relative">
|
||||||
|
|
BIN
web/static/no-thumbnail.png
Normal file
BIN
web/static/no-thumbnail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 584 B |
|
@ -8,17 +8,9 @@ const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({ out: 'build' }),
|
adapter: adapter({ out: 'build' }),
|
||||||
methodOverride: {
|
methodOverride: {
|
||||||
allowed: ['PATCH', 'DELETE'],
|
allowed: ['PATCH', 'DELETE']
|
||||||
},
|
}
|
||||||
vite: {
|
}
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
|
|
||||||
'@api': path.resolve('./src/api'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -19,9 +19,15 @@
|
||||||
"importsNotUsedAsValues": "preserve",
|
"importsNotUsedAsValues": "preserve",
|
||||||
"preserveValueImports": false,
|
"preserveValueImports": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"$lib": ["src/lib"],
|
"$lib": [
|
||||||
"$lib/*": ["src/lib/*"],
|
"src/lib"
|
||||||
"@api": ["src/api"]
|
],
|
||||||
|
"$lib/*": [
|
||||||
|
"src/lib/*"
|
||||||
|
],
|
||||||
|
"@api": [
|
||||||
|
"src/api"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
15
web/vite.config.js
Normal file
15
web/vite.config.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/** @type {import('vite').UserConfig} */
|
||||||
|
const config = {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
|
||||||
|
'@api': path.resolve('./src/api')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
Loading…
Reference in a new issue