mirror of
https://github.com/immich-app/immich.git
synced 2024-12-22 01:47:08 +02: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
@ -123,7 +123,7 @@ export class AssetController {
|
||||
|
||||
@Get('/')
|
||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
|
||||
return await this.assetService.getAllAssetsNoPagination(authUser);
|
||||
return await this.assetService.getAllAssets(authUser);
|
||||
}
|
||||
|
||||
@Get('/:deviceId')
|
||||
|
@ -61,13 +61,17 @@ export class AssetService {
|
||||
return res;
|
||||
}
|
||||
|
||||
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
|
||||
public async getAllAssets(authUser: AuthUserDto) {
|
||||
try {
|
||||
return await this.assetRepository
|
||||
.createQueryBuilder('a')
|
||||
.where('a."userId" = :userId', { userId: authUser.id })
|
||||
.orderBy('a."createdAt"::date', 'DESC')
|
||||
.getMany();
|
||||
return await this.assetRepository.find({
|
||||
where: {
|
||||
userId: authUser.id
|
||||
},
|
||||
relations: ['exifInfo'],
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
Logger.error(e, 'getAllAssets');
|
||||
}
|
||||
@ -100,8 +104,18 @@ export class AssetService {
|
||||
const asset = await this.findOne(query.did, query.aid);
|
||||
|
||||
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);
|
||||
} else {
|
||||
const { size } = await fileInfo(asset.resizePath);
|
||||
res.set({
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Content-Length': size,
|
||||
});
|
||||
file = createReadStream(asset.resizePath);
|
||||
}
|
||||
|
||||
|
@ -147,15 +147,16 @@ export class UserService {
|
||||
async getUserProfileImage(userId: string, res: Res) {
|
||||
try {
|
||||
const user = await this.userRepository.findOne({ id: userId })
|
||||
|
||||
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({
|
||||
'Content-Type': 'image/jpeg',
|
||||
});
|
||||
|
||||
const fileStream = createReadStream(user.profileImagePath)
|
||||
return new StreamableFile(fileStream);
|
||||
} catch (e) {
|
||||
|
202
web/package-lock.json
generated
202
web/package-lock.json
generated
@ -8,7 +8,9 @@
|
||||
"name": "web",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"cookie": "^0.4.2",
|
||||
"leaflet": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
@ -18,8 +20,10 @@
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/leaflet": "^1.7.10",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@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": {
|
||||
"version": "5.0.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"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": {
|
||||
"version": "4.14.182",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
||||
@ -599,6 +628,11 @@
|
||||
"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": {
|
||||
"version": "10.4.7",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz",
|
||||
@ -632,6 +666,15 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -802,6 +845,17 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -880,6 +934,14 @@
|
||||
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
|
||||
"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": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
||||
@ -1653,6 +1715,38 @@
|
||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||
@ -1947,6 +2041,11 @@
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@ -2031,6 +2130,25 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
@ -3276,6 +3394,15 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"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": {
|
||||
"version": "4.14.182",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
|
||||
@ -3521,6 +3663,11 @@
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"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": {
|
||||
"version": "10.4.7",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.7.tgz",
|
||||
@ -3535,6 +3682,15 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -3644,6 +3800,14 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -3699,6 +3863,11 @@
|
||||
"integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
|
||||
"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": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
|
||||
@ -4192,6 +4361,21 @@
|
||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||
@ -4412,6 +4596,11 @@
|
||||
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@ -4478,6 +4667,19 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
|
@ -16,8 +16,10 @@
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.73",
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/leaflet": "^1.7.10",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||
@ -38,7 +40,9 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"cookie": "^0.4.2",
|
||||
"leaflet": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"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 { fade } from 'svelte/transition';
|
||||
import { serverEndpoint } from '../../constants';
|
||||
|
||||
import IntersectionObserver from '$lib/components/photos/intersection-observer.svelte';
|
||||
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
|
||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
|
||||
|
||||
@ -57,6 +56,7 @@
|
||||
return videoData;
|
||||
}
|
||||
};
|
||||
|
||||
const parseVideoDuration = (duration: string) => {
|
||||
const timePart = duration.split(':');
|
||||
const hours = timePart[0];
|
||||
@ -71,11 +71,21 @@
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<IntersectionObserver once={true} let:intersecting>
|
||||
<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:mouseleave={() => (mouseOver = false)}
|
||||
on:click={() => dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })}
|
||||
@ -104,13 +114,12 @@
|
||||
|
||||
{#if intersecting}
|
||||
{#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}
|
||||
<img
|
||||
in:fade={{ duration: 200 }}
|
||||
src={imageData}
|
||||
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"
|
||||
/>
|
||||
{/await}
|
@ -3,12 +3,13 @@
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
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 LoadingSpinner from '../shared/loading-spinner.svelte';
|
||||
|
||||
export let assetId: string;
|
||||
export let deviceId: string;
|
||||
|
||||
let assetInfo: ImmichAsset;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@ -41,22 +42,18 @@
|
||||
};
|
||||
</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}
|
||||
{#await loadAssetData()}
|
||||
<div class="flex place-items-center place-content-center h-full">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:then assetData}
|
||||
<div class="flex place-items-center place-content-center h-full">
|
||||
<img
|
||||
in:fade={{ duration: 200 }}
|
||||
transition:fade={{ duration: 150 }}
|
||||
src={assetData}
|
||||
alt={assetId}
|
||||
class="object-cover h-full transition-all duration-100 z-0"
|
||||
class="object-contain h-full transition-all"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
</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">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { ImmichUser } from '$lib/models/immich-user';
|
||||
import { onMount } from 'svelte';
|
||||
@ -11,13 +12,19 @@
|
||||
let shouldShowProfileImage = false;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const getFirstLetter = (text?: string) => {
|
||||
return text?.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
const navigateToAdmin = () => {
|
||||
console.log('Navigating to admin page');
|
||||
goto('/admin');
|
||||
};
|
||||
</script>
|
||||
|
||||
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
|
||||
@ -33,11 +40,11 @@
|
||||
<!-- <div>Upload</div> -->
|
||||
|
||||
{#if user.isAdmin}
|
||||
<a
|
||||
<button
|
||||
class={`hover:text-immich-primary font-medium ${
|
||||
$page.url.pathname == '/admin' && 'text-immich-primary underline'
|
||||
}`}
|
||||
href="/admin">Administration</a
|
||||
on:click={navigateToAdmin}>Administration</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
|
@ -18,8 +18,8 @@
|
||||
|
||||
<div
|
||||
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
|
||||
${isSelected && 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/50'}
|
||||
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/25'}
|
||||
`}
|
||||
>
|
||||
<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">
|
||||
import '../app.css';
|
||||
|
||||
import { fly, slide, blur } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { getRequest } from '$lib/api';
|
||||
import { onDestroy } from 'svelte';
|
||||
import '../app.css';
|
||||
import { serverEndpoint } from '../lib/constants';
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
|
||||
export let url: string;
|
||||
let endpoint = serverEndpoint;
|
||||
let isServerOk = true;
|
||||
|
||||
@ -18,7 +29,12 @@
|
||||
</script>
|
||||
|
||||
<main>
|
||||
{#key url}
|
||||
<div transition:blur={{ duration: 250 }}>
|
||||
<slot />
|
||||
<DownloadPanel />
|
||||
</div>
|
||||
{/key}
|
||||
</main>
|
||||
|
||||
<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 SideBarButton from '$lib/components/shared/side-bar-button.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 { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { session } from '$app/stores';
|
||||
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 PhotoViewer from '../../lib/components/photos/photo_viewer.svelte';
|
||||
import type { ImmichAsset } from '../../lib/models/immich-asset';
|
||||
import { AssetType } from '../../lib/models/immich-asset';
|
||||
import LoadingSpinner from '../../lib/components/shared/loading-spinner.svelte';
|
||||
import AssetViewer from '../../lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import DownloadPanel from '../../lib/components/asset-viewer/download-panel.svelte';
|
||||
|
||||
export let user: ImmichUser;
|
||||
let selectedAction: AppSideBarSelection;
|
||||
@ -52,8 +50,6 @@
|
||||
}
|
||||
|
||||
let isShowAsset = false;
|
||||
let viewDeviceId: string = '';
|
||||
let viewAssetId: string = '';
|
||||
let currentViewAssetIndex = 0;
|
||||
let currentSelectedAsset: ImmichAsset;
|
||||
|
||||
@ -78,30 +74,10 @@
|
||||
const viewAssetHandler = (event: CustomEvent) => {
|
||||
const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail;
|
||||
|
||||
viewDeviceId = deviceId;
|
||||
viewAssetId = assetId;
|
||||
|
||||
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
|
||||
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
|
||||
isShowAsset = true;
|
||||
};
|
||||
|
||||
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];
|
||||
// pushState(assetId);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -137,7 +113,7 @@
|
||||
on:mouseleave={() => (isMouseOverGroup = false)}
|
||||
>
|
||||
<!-- 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}
|
||||
<div
|
||||
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||
@ -152,7 +128,7 @@
|
||||
</p>
|
||||
|
||||
<!-- Image grid -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each assetsInDateGroup as asset}
|
||||
<ImmichThumbnail
|
||||
{asset}
|
||||
@ -171,34 +147,9 @@
|
||||
|
||||
<!-- Overlay Asset Viewer -->
|
||||
{#if isShowAsset}
|
||||
<section
|
||||
class="absolute w-screen h-screen top-0 overflow-y-hidden bg-black z-[9999] flex justify-between place-items-center"
|
||||
>
|
||||
<button
|
||||
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>
|
||||
<AssetViewer
|
||||
selectedAsset={currentSelectedAsset}
|
||||
selectedIndex={currentViewAssetIndex}
|
||||
on:close={() => (isShowAsset = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user