mirror of
https://github.com/immich-app/immich.git
synced 2025-01-28 06:32:44 +01:00
View assets detail and download operation (#198)
* Fixed not displaying default user profile picture * Added buttons to close viewer and micro-interaction for navigating assets left, right * Add additional buttons to the control bar * Display EXIF info * Added map to detail info * Handle user input keyboard * Fixed incorrect file name when downloading multiple files * Implemented download panel
This commit is contained in:
parent
6924aa5eb1
commit
53c3c916a6
19 changed files with 798 additions and 100 deletions
server/src/api-v1
web
|
@ -123,7 +123,7 @@ export class AssetController {
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
|
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
|
||||||
return await this.assetService.getAllAssetsNoPagination(authUser);
|
return await this.assetService.getAllAssets(authUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:deviceId')
|
@Get('/:deviceId')
|
||||||
|
|
|
@ -61,13 +61,17 @@ export class AssetService {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
|
public async getAllAssets(authUser: AuthUserDto) {
|
||||||
try {
|
try {
|
||||||
return await this.assetRepository
|
return await this.assetRepository.find({
|
||||||
.createQueryBuilder('a')
|
where: {
|
||||||
.where('a."userId" = :userId', { userId: authUser.id })
|
userId: authUser.id
|
||||||
.orderBy('a."createdAt"::date', 'DESC')
|
},
|
||||||
.getMany();
|
relations: ['exifInfo'],
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC'
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(e, 'getAllAssets');
|
Logger.error(e, 'getAllAssets');
|
||||||
}
|
}
|
||||||
|
@ -100,8 +104,18 @@ export class AssetService {
|
||||||
const asset = await this.findOne(query.did, query.aid);
|
const asset = await this.findOne(query.did, query.aid);
|
||||||
|
|
||||||
if (query.isThumb === 'false' || !query.isThumb) {
|
if (query.isThumb === 'false' || !query.isThumb) {
|
||||||
|
const { size } = await fileInfo(asset.originalPath);
|
||||||
|
res.set({
|
||||||
|
'Content-Type': asset.mimeType,
|
||||||
|
'Content-Length': size,
|
||||||
|
});
|
||||||
file = createReadStream(asset.originalPath);
|
file = createReadStream(asset.originalPath);
|
||||||
} else {
|
} else {
|
||||||
|
const { size } = await fileInfo(asset.resizePath);
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
'Content-Length': size,
|
||||||
|
});
|
||||||
file = createReadStream(asset.resizePath);
|
file = createReadStream(asset.resizePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -147,15 +147,16 @@ export class UserService {
|
||||||
async getUserProfileImage(userId: string, res: Res) {
|
async getUserProfileImage(userId: string, res: Res) {
|
||||||
try {
|
try {
|
||||||
const user = await this.userRepository.findOne({ id: userId })
|
const user = await this.userRepository.findOne({ id: userId })
|
||||||
|
|
||||||
if (!user.profileImagePath) {
|
if (!user.profileImagePath) {
|
||||||
console.log("empty return")
|
// throw new BadRequestException('User does not have a profile image');
|
||||||
throw new BadRequestException('User does not have a profile image');
|
res.status(404).send('User does not have a profile image');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'image/jpeg',
|
'Content-Type': 'image/jpeg',
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileStream = createReadStream(user.profileImagePath)
|
const fileStream = createReadStream(user.profileImagePath)
|
||||||
return new StreamableFile(fileStream);
|
return new StreamableFile(fileStream);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
202
web/package-lock.json
generated
202
web/package-lock.json
generated
|
@ -8,7 +8,9 @@
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
|
@ -18,8 +20,10 @@
|
||||||
"@sveltejs/adapter-auto": "next",
|
"@sveltejs/adapter-auto": "next",
|
||||||
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
||||||
"@sveltejs/kit": "next",
|
"@sveltejs/kit": "next",
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
|
"@types/leaflet": "^1.7.10",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
|
@ -231,6 +235,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/axios": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
|
||||||
|
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/bcrypt": {
|
"node_modules/@types/bcrypt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
|
||||||
|
@ -246,12 +260,27 @@
|
||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
|
||||||
|
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/leaflet": {
|
||||||
|
"version": "1.7.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.7.10.tgz",
|
||||||
|
"integrity": "sha512-RzK5BYwYboOXXxyF01tp8g1J8UbdRvoaf+F/jCnVaWC42+QITB6wKvUklcX7jCMRWkzTnGO9NLg7A6SzrlGALA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.14.182",
|
"version": "4.14.182",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
||||||
|
@ -599,6 +628,11 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.7",
|
"version": "10.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz",
|
||||||
|
@ -632,6 +666,15 @@
|
||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.9",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
@ -802,6 +845,17 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
@ -880,6 +934,14 @@
|
||||||
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
|
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-indent": {
|
"node_modules/detect-indent": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
||||||
|
@ -1653,6 +1715,38 @@
|
||||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
|
@ -1947,6 +2041,11 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
@ -2031,6 +2130,25 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/min-indent": {
|
"node_modules/min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
|
@ -3276,6 +3394,15 @@
|
||||||
"svelte-hmr": "^0.14.11"
|
"svelte-hmr": "^0.14.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/axios": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"axios": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/bcrypt": {
|
"@types/bcrypt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
|
||||||
|
@ -3291,12 +3418,27 @@
|
||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/geojson": {
|
||||||
|
"version": "7946.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
|
||||||
|
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/leaflet": {
|
||||||
|
"version": "1.7.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.7.10.tgz",
|
||||||
|
"integrity": "sha512-RzK5BYwYboOXXxyF01tp8g1J8UbdRvoaf+F/jCnVaWC42+QITB6wKvUklcX7jCMRWkzTnGO9NLg7A6SzrlGALA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.182",
|
"version": "4.14.182",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
||||||
|
@ -3521,6 +3663,11 @@
|
||||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"autoprefixer": {
|
"autoprefixer": {
|
||||||
"version": "10.4.7",
|
"version": "10.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz",
|
||||||
|
@ -3535,6 +3682,15 @@
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.14.9",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
@ -3644,6 +3800,14 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"requires": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
@ -3699,6 +3863,11 @@
|
||||||
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
|
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||||
|
},
|
||||||
"detect-indent": {
|
"detect-indent": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
||||||
|
@ -4192,6 +4361,21 @@
|
||||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"follow-redirects": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||||
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fraction.js": {
|
"fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
|
@ -4412,6 +4596,11 @@
|
||||||
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
|
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"leaflet": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
|
||||||
|
},
|
||||||
"levn": {
|
"levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
@ -4478,6 +4667,19 @@
|
||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"requires": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"min-indent": {
|
"min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
"@sveltejs/adapter-auto": "next",
|
"@sveltejs/adapter-auto": "next",
|
||||||
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
||||||
"@sveltejs/kit": "next",
|
"@sveltejs/kit": "next",
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
|
"@types/leaflet": "^1.7.10",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
|
@ -38,7 +40,9 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||||
|
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||||
|
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||||
|
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||||
|
import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
|
||||||
|
import CircleIconButton from '../shared/circle_icon_button.svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-16 bg-black/5 flex justify-between place-items-center px-3 transition-transform duration-200 z-[9999]">
|
||||||
|
<div>
|
||||||
|
<CircleIconButton logo={ArrowLeft} on:click={() => dispatch('goBack')} />
|
||||||
|
</div>
|
||||||
|
<div class="text-white flex gap-2">
|
||||||
|
<CircleIconButton logo={CloudDownloadOutline} on:click={() => dispatch('download')} />
|
||||||
|
<!-- <CircleIconButton logo={DotsVertical} on:click={() => console.log('Options')} /> -->
|
||||||
|
<CircleIconButton logo={InformationOutline} on:click={() => dispatch('showDetail')} />
|
||||||
|
</div>
|
||||||
|
</div>
|
234
web/src/lib/components/asset-viewer/asset-viewer.svelte
Normal file
234
web/src/lib/components/asset-viewer/asset-viewer.svelte
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
|
import { fly, slide } from 'svelte/transition';
|
||||||
|
import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
|
||||||
|
import { flattenAssetGroupByDate } from '$lib/stores/assets';
|
||||||
|
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||||
|
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||||
|
import { AssetType, type ImmichAsset, type ImmichExif } from '../../models/immich-asset';
|
||||||
|
import PhotoViewer from './photo-viewer.svelte';
|
||||||
|
import DetailPanel from './detail-panel.svelte';
|
||||||
|
import { session } from '$app/stores';
|
||||||
|
import { serverEndpoint } from '../../constants';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { downloadAssets } from '$lib/stores/download';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let selectedAsset: ImmichAsset;
|
||||||
|
export let selectedIndex: number;
|
||||||
|
|
||||||
|
let viewDeviceId: string;
|
||||||
|
let viewAssetId: string;
|
||||||
|
|
||||||
|
let halfLeftHover = false;
|
||||||
|
let halfRightHover = false;
|
||||||
|
let isShowDetail = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
viewAssetId = selectedAsset.id;
|
||||||
|
viewDeviceId = selectedAsset.deviceId;
|
||||||
|
pushState(viewAssetId);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
document.removeEventListener('keydown', (b) => {
|
||||||
|
console.log('destroyed', b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKeyboardPress = (key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'Escape':
|
||||||
|
closeViewer();
|
||||||
|
return;
|
||||||
|
case 'i':
|
||||||
|
isShowDetail = !isShowDetail;
|
||||||
|
return;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
navigateAssetBackward();
|
||||||
|
return;
|
||||||
|
case 'ArrowRight':
|
||||||
|
navigateAssetForward();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeViewer = () => {
|
||||||
|
history.pushState(null, '', `/photos`);
|
||||||
|
dispatch('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateAssetForward = () => {
|
||||||
|
const nextAsset = $flattenAssetGroupByDate[selectedIndex + 1];
|
||||||
|
viewDeviceId = nextAsset.deviceId;
|
||||||
|
viewAssetId = nextAsset.id;
|
||||||
|
|
||||||
|
selectedIndex = selectedIndex + 1;
|
||||||
|
selectedAsset = $flattenAssetGroupByDate[selectedIndex];
|
||||||
|
pushState(viewAssetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateAssetBackward = () => {
|
||||||
|
const lastAsset = $flattenAssetGroupByDate[selectedIndex - 1];
|
||||||
|
viewDeviceId = lastAsset.deviceId;
|
||||||
|
viewAssetId = lastAsset.id;
|
||||||
|
|
||||||
|
selectedIndex = selectedIndex - 1;
|
||||||
|
selectedAsset = $flattenAssetGroupByDate[selectedIndex];
|
||||||
|
pushState(viewAssetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushState = (assetId: string) => {
|
||||||
|
// add a URL to the browser's history
|
||||||
|
// changes the current URL in the address bar but doesn't perform any SvelteKit navigation
|
||||||
|
history.pushState(null, '', `/photos/${assetId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDetailInfoHandler = () => {
|
||||||
|
isShowDetail = !isShowDetail;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = async () => {
|
||||||
|
if ($session.user) {
|
||||||
|
const url = `${serverEndpoint}/asset/download?aid=${selectedAsset.deviceAssetId}&did=${selectedAsset.deviceId}&isThumb=false`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imageName = selectedAsset.exifInfo?.imageName ? selectedAsset.exifInfo?.imageName : selectedAsset.id;
|
||||||
|
const imageExtension = selectedAsset.originalPath.split('.')[1];
|
||||||
|
const imageFileName = imageName + '.' + imageExtension;
|
||||||
|
|
||||||
|
// If assets is already download -> return;
|
||||||
|
if ($downloadAssets[imageFileName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$downloadAssets[imageFileName] = 0;
|
||||||
|
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + $session.user.accessToken,
|
||||||
|
},
|
||||||
|
onDownloadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.lengthComputable) {
|
||||||
|
const total = progressEvent.total;
|
||||||
|
const current = progressEvent.loaded;
|
||||||
|
let percentCompleted = Math.floor((current / total) * 100);
|
||||||
|
|
||||||
|
$downloadAssets[imageFileName] = percentCompleted;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const fileUrl = URL.createObjectURL(new Blob([res.data]));
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = fileUrl;
|
||||||
|
anchor.download = imageFileName;
|
||||||
|
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
|
||||||
|
URL.revokeObjectURL(fileUrl);
|
||||||
|
|
||||||
|
// Remove item from download list
|
||||||
|
setTimeout(() => {
|
||||||
|
const copy = $downloadAssets;
|
||||||
|
delete copy[imageFileName];
|
||||||
|
$downloadAssets = copy;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error downloading file ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="immich-asset-viewer"
|
||||||
|
class="absolute h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 "
|
||||||
|
>
|
||||||
|
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
|
||||||
|
<AsserViewerNavBar on:goBack={closeViewer} on:showDetail={showDetailInfoHandler} on:download={downloadFile} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row-start-2 row-span-end col-start-1- col-span-full z-[1000] flex place-items-center hover:cursor-pointer w-3/4"
|
||||||
|
on:mouseenter={() => {
|
||||||
|
halfLeftHover = true;
|
||||||
|
halfRightHover = false;
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
halfLeftHover = false;
|
||||||
|
}}
|
||||||
|
on:click={navigateAssetBackward}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
|
||||||
|
class:navigation-button-hover={halfLeftHover}
|
||||||
|
on:click={navigateAssetBackward}
|
||||||
|
>
|
||||||
|
<ChevronLeft size="36" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-start-1 row-span-full col-start-1 col-span-4">
|
||||||
|
{#key selectedIndex}
|
||||||
|
{#if viewAssetId && viewDeviceId}
|
||||||
|
{#if selectedAsset.type == AssetType.IMAGE}
|
||||||
|
<PhotoViewer assetId={viewAssetId} deviceId={viewDeviceId} on:close={closeViewer} />
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="w-full h-full bg-immich-primary/10 flex flex-col place-items-center place-content-center "
|
||||||
|
on:click={closeViewer}
|
||||||
|
>
|
||||||
|
<h1 class="animate-pulse font-bold text-4xl">Video viewer is under construction</h1>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row-start-2 row-span-full col-start-3 col-span-2 z-[1000] flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end"
|
||||||
|
on:click={navigateAssetForward}
|
||||||
|
on:mouseenter={() => {
|
||||||
|
halfLeftHover = false;
|
||||||
|
halfRightHover = true;
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
halfRightHover = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
|
||||||
|
class:navigation-button-hover={halfRightHover}
|
||||||
|
on:click={navigateAssetForward}
|
||||||
|
>
|
||||||
|
<ChevronRight size="36" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isShowDetail}
|
||||||
|
<div
|
||||||
|
transition:fly={{ duration: 150 }}
|
||||||
|
id="detail-panel"
|
||||||
|
class="bg-immich-bg w-[360px] row-span-full transition-all "
|
||||||
|
translate="yes"
|
||||||
|
>
|
||||||
|
<DetailPanel asset={selectedAsset} on:close={() => (isShowDetail = false)} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.navigation-button-hover {
|
||||||
|
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||||
|
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||||
|
transition: all 150ms;
|
||||||
|
}
|
||||||
|
</style>
|
164
web/src/lib/components/asset-viewer/detail-panel.svelte
Normal file
164
web/src/lib/components/asset-viewer/detail-panel.svelte
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Close from 'svelte-material-icons/Close.svelte';
|
||||||
|
import Calendar from 'svelte-material-icons/Calendar.svelte';
|
||||||
|
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||||
|
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
|
||||||
|
import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
|
||||||
|
import moment from 'moment';
|
||||||
|
import type { ImmichAsset } from '../../models/immich-asset';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
|
// Map Property
|
||||||
|
let map: any;
|
||||||
|
let leaflet: any;
|
||||||
|
let marker: any;
|
||||||
|
|
||||||
|
export let asset: ImmichAsset;
|
||||||
|
$: {
|
||||||
|
// Redraw map
|
||||||
|
if (map && leaflet) {
|
||||||
|
map.removeLayer(marker);
|
||||||
|
map.setView([asset.exifInfo?.latitude || 0, asset.exifInfo?.longitude || 0], 17);
|
||||||
|
marker = leaflet.marker([asset.exifInfo?.latitude || 0, asset.exifInfo?.longitude || 0]);
|
||||||
|
map.addLayer(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser) {
|
||||||
|
// @ts-ignore
|
||||||
|
leaflet = await import('leaflet');
|
||||||
|
map = leaflet.map('map').setView([asset.exifInfo?.latitude || 0, asset.exifInfo?.longitude || 0], 17);
|
||||||
|
|
||||||
|
leaflet
|
||||||
|
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
})
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
marker = leaflet.marker([asset.exifInfo?.latitude || 0, asset.exifInfo?.longitude || 0]);
|
||||||
|
|
||||||
|
map.addLayer(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const getHumanReadableString = (sizeInByte: number) => {
|
||||||
|
const pepibyte = 1.126 * Math.pow(10, 15);
|
||||||
|
const tebibyte = 1.1 * Math.pow(10, 12);
|
||||||
|
const gibibyte = 1.074 * Math.pow(10, 9);
|
||||||
|
const mebibyte = 1.049 * Math.pow(10, 6);
|
||||||
|
const kibibyte = 1024;
|
||||||
|
// Pebibyte
|
||||||
|
if (sizeInByte >= pepibyte) {
|
||||||
|
// Pe
|
||||||
|
return `${(sizeInByte / pepibyte).toFixed(1)}PB`;
|
||||||
|
} else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) {
|
||||||
|
// Te
|
||||||
|
return `${(sizeInByte / tebibyte).toFixed(1)}TB`;
|
||||||
|
} else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) {
|
||||||
|
// Gi
|
||||||
|
return `${(sizeInByte / gibibyte).toFixed(1)}GB`;
|
||||||
|
} else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) {
|
||||||
|
// Mega
|
||||||
|
return `${(sizeInByte / mebibyte).toFixed(1)}MB`;
|
||||||
|
} else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) {
|
||||||
|
// Kibi
|
||||||
|
return `${(sizeInByte / kibibyte).toFixed(1)}KB`;
|
||||||
|
} else {
|
||||||
|
return `${sizeInByte}B`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="p-2">
|
||||||
|
<div class="flex place-items-center gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded-full p-3 flex place-items-center place-content-center hover:bg-gray-200 transition-colors"
|
||||||
|
on:click={() => dispatch('close')}
|
||||||
|
>
|
||||||
|
<Close size="24" color="#232323" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p class="text-black text-lg">Info</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 py-4">
|
||||||
|
<p class="text-sm pb-4">DETAILS</p>
|
||||||
|
|
||||||
|
{#if asset.exifInfo?.dateTimeOriginal}
|
||||||
|
<div class="flex gap-4 py-4">
|
||||||
|
<div>
|
||||||
|
<Calendar size="24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{moment(asset.exifInfo.dateTimeOriginal).format('MMM DD')}</p>
|
||||||
|
<div class="flex gap-2 text-sm">
|
||||||
|
<p>
|
||||||
|
{moment(
|
||||||
|
asset.exifInfo.dateTimeOriginal
|
||||||
|
.toString()
|
||||||
|
.slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1),
|
||||||
|
).format('ddd, hh:mm A')}
|
||||||
|
</p>
|
||||||
|
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>{/if}
|
||||||
|
|
||||||
|
{#if asset.exifInfo?.fileSizeInByte}
|
||||||
|
<div class="flex gap-4 py-4">
|
||||||
|
<div><ImageOutline size="24" /></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{`${asset.exifInfo.imageName}.${asset.originalPath.split('.')[1]}` || ''}</p>
|
||||||
|
<div class="flex text-sm gap-2">
|
||||||
|
<p>{((asset.exifInfo.exifImageHeight * asset.exifInfo.exifImageWidth) / 1_000_000).toFixed(0)}MP</p>
|
||||||
|
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>
|
||||||
|
<p>{getHumanReadableString(asset.exifInfo.fileSizeInByte)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if asset.exifInfo?.fNumber}
|
||||||
|
<div class="flex gap-4 py-4">
|
||||||
|
<div><CameraIris size="24" /></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
|
||||||
|
<div class="flex text-sm gap-2">
|
||||||
|
<p>{`f/${asset.exifInfo.fNumber}` || ''}</p>
|
||||||
|
<p>{`1/${1 / asset.exifInfo.exposureTime}` || ''}</p>
|
||||||
|
<p>{`${asset.exifInfo.focalLength}mm` || ''}</p>
|
||||||
|
<p>{`ISO${asset.exifInfo.iso}` || ''}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if asset.exifInfo?.city}
|
||||||
|
<div class="flex gap-4 py-4">
|
||||||
|
<div><MapMarkerOutline size="24" /></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{asset.exifInfo.city}</p>
|
||||||
|
<div class="flex text-sm gap-2">
|
||||||
|
<p>{asset.exifInfo.state},</p>
|
||||||
|
<p>{asset.exifInfo.country}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={`${asset.exifInfo?.latitude ? 'visible' : 'hidden'}`}>
|
||||||
|
<div class="h-[360px] w-full" id="map" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.css';
|
||||||
|
</style>
|
26
web/src/lib/components/asset-viewer/download-panel.svelte
Normal file
26
web/src/lib/components/asset-viewer/download-panel.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { downloadAssets, isDownloading } from '$lib/stores/download';
|
||||||
|
import { fly, slide } from 'svelte/transition';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $isDownloading}
|
||||||
|
<div
|
||||||
|
transition:fly={{ x: -100, duration: 350 }}
|
||||||
|
class="w-[315px] max-h-[270px] bg-immich-bg border rounded-2xl shadow-sm absolute bottom-10 left-2 p-4 z-[10000] text-sm"
|
||||||
|
>
|
||||||
|
<p class="text-gray-500 text-xs mb-2">DOWNLOADING</p>
|
||||||
|
<div class="max-h-[200px] my-2 overflow-y-auto mb-2 flex flex-col text-sm">
|
||||||
|
{#each Object.keys($downloadAssets) as fileName}
|
||||||
|
<div class="mb-2" transition:slide>
|
||||||
|
<p class="font-medium text-xs truncate">■ {fileName}</p>
|
||||||
|
<div class="flex flex-row-reverse place-items-center gap-5">
|
||||||
|
<p><span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100</p>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
|
||||||
|
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${$downloadAssets[fileName]}%`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -4,8 +4,7 @@
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { serverEndpoint } from '../../constants';
|
import { serverEndpoint } from '../../constants';
|
||||||
|
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
|
||||||
import IntersectionObserver from '$lib/components/photos/intersection-observer.svelte';
|
|
||||||
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';
|
||||||
|
|
||||||
|
@ -57,6 +56,7 @@
|
||||||
return videoData;
|
return videoData;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseVideoDuration = (duration: string) => {
|
const parseVideoDuration = (duration: string) => {
|
||||||
const timePart = duration.split(':');
|
const timePart = duration.split(':');
|
||||||
const hours = timePart[0];
|
const hours = timePart[0];
|
||||||
|
@ -71,11 +71,21 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onDestroy(() => URL.revokeObjectURL(imageContent));
|
onDestroy(() => URL.revokeObjectURL(imageContent));
|
||||||
|
|
||||||
|
const getSize = () => {
|
||||||
|
if (asset.exifInfo?.orientation === 'Rotate 90 CW') {
|
||||||
|
return 'w-[176px] h-[235px]';
|
||||||
|
} else if (asset.exifInfo?.orientation === 'Horizontal (normal)') {
|
||||||
|
return 'w-[313px] h-[235px]';
|
||||||
|
} else {
|
||||||
|
return 'w-[235px] h-[235px]';
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IntersectionObserver once={true} let:intersecting>
|
<IntersectionObserver once={true} let:intersecting>
|
||||||
<div
|
<div
|
||||||
class="h-[200px] w-[200px] bg-gray-100 relative hover:cursor-pointer"
|
class={`bg-gray-100 relative hover:cursor-pointer ${getSize()}`}
|
||||||
on:mouseenter={() => (mouseOver = true)}
|
on:mouseenter={() => (mouseOver = true)}
|
||||||
on:mouseleave={() => (mouseOver = false)}
|
on:mouseleave={() => (mouseOver = false)}
|
||||||
on:click={() => dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })}
|
on:click={() => dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })}
|
||||||
|
@ -104,13 +114,12 @@
|
||||||
|
|
||||||
{#if intersecting}
|
{#if intersecting}
|
||||||
{#await loadImageData()}
|
{#await loadImageData()}
|
||||||
<div class="bg-immich-primary/10 h-[200px] w-[200px] flex place-items-center place-content-center">...</div>
|
<div class={`bg-immich-primary/10 ${getSize()} flex place-items-center place-content-center`}>...</div>
|
||||||
{:then imageData}
|
{:then imageData}
|
||||||
<img
|
<img
|
||||||
in:fade={{ duration: 200 }}
|
|
||||||
src={imageData}
|
src={imageData}
|
||||||
alt={asset.id}
|
alt={asset.id}
|
||||||
class="object-cover h-[200px] w-[200px] transition-all duration-100 z-0"
|
class={`object-cover ${getSize()} transition-all duration-100 z-0`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
{/await}
|
{/await}
|
|
@ -3,12 +3,13 @@
|
||||||
import { serverEndpoint } from '$lib/constants';
|
import { serverEndpoint } from '$lib/constants';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
import type { ImmichAsset } from '$lib/models/immich-asset';
|
import type { ImmichAsset, ImmichExif } from '$lib/models/immich-asset';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import LoadingSpinner from '../shared/loading-spinner.svelte';
|
import LoadingSpinner from '../shared/loading-spinner.svelte';
|
||||||
|
|
||||||
export let assetId: string;
|
export let assetId: string;
|
||||||
export let deviceId: string;
|
export let deviceId: string;
|
||||||
|
|
||||||
let assetInfo: ImmichAsset;
|
let assetInfo: ImmichAsset;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
@ -41,22 +42,18 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div on:click={() => dispatch('close')} class="h-screen">
|
<div transition:fade={{ duration: 150 }} class="flex place-items-center place-content-center h-full select-none">
|
||||||
{#if assetInfo}
|
{#if assetInfo}
|
||||||
{#await loadAssetData()}
|
{#await loadAssetData()}
|
||||||
<div class="flex place-items-center place-content-center h-full">
|
<LoadingSpinner />
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
{:then assetData}
|
{:then assetData}
|
||||||
<div class="flex place-items-center place-content-center h-full">
|
<img
|
||||||
<img
|
transition:fade={{ duration: 150 }}
|
||||||
in:fade={{ duration: 200 }}
|
src={assetData}
|
||||||
src={assetData}
|
alt={assetId}
|
||||||
alt={assetId}
|
class="object-contain h-full transition-all"
|
||||||
class="object-cover h-full transition-all duration-100 z-0"
|
loading="lazy"
|
||||||
loading="lazy"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
18
web/src/lib/components/shared/circle_icon_button.svelte
Normal file
18
web/src/lib/components/shared/circle_icon_button.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let logo: any;
|
||||||
|
export let backgroundColor: string = '';
|
||||||
|
export let logoColor: string = '';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="rounded-full p-3 flex place-items-center place-content-center text-gray-50 hover:bg-gray-800"
|
||||||
|
class:background-color={backgroundColor}
|
||||||
|
class:color={logoColor}
|
||||||
|
on:click={() => dispatch('click')}
|
||||||
|
>
|
||||||
|
<svelte:component this={logo} size="24" />
|
||||||
|
</button>
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { ImmichUser } from '$lib/models/immich-user';
|
import type { ImmichUser } from '$lib/models/immich-user';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
@ -11,13 +12,19 @@
|
||||||
let shouldShowProfileImage = false;
|
let shouldShowProfileImage = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`);
|
const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`, { method: 'GET' });
|
||||||
|
|
||||||
if (res.status == 200) shouldShowProfileImage = true;
|
if (res.status == 200) shouldShowProfileImage = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getFirstLetter = (text?: string) => {
|
const getFirstLetter = (text?: string) => {
|
||||||
return text?.charAt(0).toUpperCase();
|
return text?.charAt(0).toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const navigateToAdmin = () => {
|
||||||
|
console.log('Navigating to admin page');
|
||||||
|
goto('/admin');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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">
|
||||||
|
@ -33,11 +40,11 @@
|
||||||
<!-- <div>Upload</div> -->
|
<!-- <div>Upload</div> -->
|
||||||
|
|
||||||
{#if user.isAdmin}
|
{#if user.isAdmin}
|
||||||
<a
|
<button
|
||||||
class={`hover:text-immich-primary font-medium ${
|
class={`hover:text-immich-primary font-medium ${
|
||||||
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
||||||
}`}
|
}`}
|
||||||
href="/admin">Administration</a
|
on:click={navigateToAdmin}>Administration</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
on:click={onButtonClicked}
|
on:click={onButtonClicked}
|
||||||
class={`flex gap-4 place-items-center pl-5 py-3 rounded-tr-xl rounded-br-xl hover:bg-gray-200 hover:text-immich-primary hover:cursor-pointer
|
class={`flex gap-4 place-items-center pl-5 py-3 rounded-tr-full rounded-br-full hover:bg-gray-200 hover:text-immich-primary hover:cursor-pointer
|
||||||
${isSelected && 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/50'}
|
${isSelected && 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/25'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<svelte:component this={logo} size="24" />
|
<svelte:component this={logo} size="24" />
|
||||||
|
|
13
web/src/lib/stores/download.ts
Normal file
13
web/src/lib/stores/download.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
|
||||||
|
export const downloadAssets = writable<Record<string, number>>({});
|
||||||
|
|
||||||
|
|
||||||
|
export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
|
||||||
|
if (Object.keys($downloadAssets).length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: Load = async ({ url }) => ({ props: { url } });
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
import { fly, slide, blur } from 'svelte/transition';
|
||||||
|
import { quintOut } from 'svelte/easing';
|
||||||
import { getRequest } from '$lib/api';
|
import { getRequest } from '$lib/api';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import '../app.css';
|
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||||
import { serverEndpoint } from '../lib/constants';
|
import { serverEndpoint } from '$lib/constants';
|
||||||
|
|
||||||
|
export let url: string;
|
||||||
let endpoint = serverEndpoint;
|
let endpoint = serverEndpoint;
|
||||||
let isServerOk = true;
|
let isServerOk = true;
|
||||||
|
|
||||||
|
@ -18,7 +29,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<slot />
|
{#key url}
|
||||||
|
<div transition:blur={{ duration: 250 }}>
|
||||||
|
<slot />
|
||||||
|
<DownloadPanel />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer
|
<footer
|
||||||
|
|
20
web/src/routes/photos/[assetId].svelte
Normal file
20
web/src/routes/photos/[assetId].svelte
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
import type { Load } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: Load = async ({ session }) => {
|
||||||
|
console.log('navigating to unknown paage');
|
||||||
|
if (!session.user) {
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/auth/login',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
redirect: '/photos',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -27,20 +27,18 @@
|
||||||
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 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 ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
|
||||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
|
||||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||||
import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fade, 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/photos/immich-thumbnail.svelte';
|
import ImmichThumbnail from '../../lib/components/asset-viewer/immich-thumbnail.svelte';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import PhotoViewer from '../../lib/components/photos/photo_viewer.svelte';
|
|
||||||
import type { ImmichAsset } from '../../lib/models/immich-asset';
|
import type { ImmichAsset } from '../../lib/models/immich-asset';
|
||||||
import { AssetType } from '../../lib/models/immich-asset';
|
import AssetViewer from '../../lib/components/asset-viewer/asset-viewer.svelte';
|
||||||
import LoadingSpinner from '../../lib/components/shared/loading-spinner.svelte';
|
import DownloadPanel from '../../lib/components/asset-viewer/download-panel.svelte';
|
||||||
|
|
||||||
export let user: ImmichUser;
|
export let user: ImmichUser;
|
||||||
let selectedAction: AppSideBarSelection;
|
let selectedAction: AppSideBarSelection;
|
||||||
|
@ -52,8 +50,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let isShowAsset = false;
|
let isShowAsset = false;
|
||||||
let viewDeviceId: string = '';
|
|
||||||
let viewAssetId: string = '';
|
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
let currentSelectedAsset: ImmichAsset;
|
let currentSelectedAsset: ImmichAsset;
|
||||||
|
|
||||||
|
@ -78,30 +74,10 @@
|
||||||
const viewAssetHandler = (event: CustomEvent) => {
|
const viewAssetHandler = (event: CustomEvent) => {
|
||||||
const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail;
|
const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail;
|
||||||
|
|
||||||
viewDeviceId = deviceId;
|
|
||||||
viewAssetId = assetId;
|
|
||||||
|
|
||||||
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
||||||
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
||||||
isShowAsset = true;
|
isShowAsset = true;
|
||||||
};
|
// pushState(assetId);
|
||||||
|
|
||||||
const navigateAssetForward = () => {
|
|
||||||
const nextAsset = $flattenAssetGroupByDate[currentViewAssetIndex + 1];
|
|
||||||
viewDeviceId = nextAsset.deviceId;
|
|
||||||
viewAssetId = nextAsset.id;
|
|
||||||
|
|
||||||
currentViewAssetIndex = currentViewAssetIndex + 1;
|
|
||||||
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateAssetBackward = () => {
|
|
||||||
const lastAsset = $flattenAssetGroupByDate[currentViewAssetIndex - 1];
|
|
||||||
viewDeviceId = lastAsset.deviceId;
|
|
||||||
viewAssetId = lastAsset.id;
|
|
||||||
|
|
||||||
currentViewAssetIndex = currentViewAssetIndex - 1;
|
|
||||||
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -137,7 +113,7 @@
|
||||||
on:mouseleave={() => (isMouseOverGroup = false)}
|
on:mouseleave={() => (isMouseOverGroup = false)}
|
||||||
>
|
>
|
||||||
<!-- Date group title -->
|
<!-- Date group title -->
|
||||||
<p class="font-medium text-sm text-immich-primary mb-2 flex place-items-center h-6">
|
<p class="font-medium text-sm text-black mb-2 flex place-items-center h-6">
|
||||||
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
|
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
|
||||||
<div
|
<div
|
||||||
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||||
|
@ -152,7 +128,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each assetsInDateGroup as asset}
|
{#each assetsInDateGroup as asset}
|
||||||
<ImmichThumbnail
|
<ImmichThumbnail
|
||||||
{asset}
|
{asset}
|
||||||
|
@ -171,34 +147,9 @@
|
||||||
|
|
||||||
<!-- Overlay Asset Viewer -->
|
<!-- Overlay Asset Viewer -->
|
||||||
{#if isShowAsset}
|
{#if isShowAsset}
|
||||||
<section
|
<AssetViewer
|
||||||
class="absolute w-screen h-screen top-0 overflow-y-hidden bg-black z-[9999] flex justify-between place-items-center"
|
selectedAsset={currentSelectedAsset}
|
||||||
>
|
selectedIndex={currentViewAssetIndex}
|
||||||
<button
|
on:close={() => (isShowAsset = false)}
|
||||||
class="rounded-full p-4 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
|
/>
|
||||||
on:click={navigateAssetBackward}
|
|
||||||
>
|
|
||||||
<ChevronLeft size="48" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#key currentViewAssetIndex}
|
|
||||||
{#if currentSelectedAsset.type == AssetType.IMAGE}
|
|
||||||
<PhotoViewer assetId={viewAssetId} deviceId={viewDeviceId} on:close={() => (isShowAsset = false)} />
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="w-full h-full bg-immich-primary/10 flex flex-col place-items-center place-content-center "
|
|
||||||
on:click={() => (isShowAsset = false)}
|
|
||||||
>
|
|
||||||
<h1 class="animate-pulse font-bold text-4xl">Video viewer is under construction</h1>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="rounded-full p-4 hover:bg-gray-500 hover:text-gray-700 bg-black text-gray-500 mx-4"
|
|
||||||
on:click={navigateAssetForward}
|
|
||||||
>
|
|
||||||
<ChevronRight size="48" />
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
Loading…
Reference in a new issue