Merge branch 'main' of github.com:immich-app/immich into new-upload

This commit is contained in:
Alex 2025-06-04 16:40:34 -05:00
commit 2b6672293b
No known key found for this signature in database
GPG Key ID: 53CD082B3A5E1082
70 changed files with 2058 additions and 1273 deletions

142
cli/package-lock.json generated
View File

@ -1370,17 +1370,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -1394,7 +1394,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -1410,16 +1410,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4"
},
"engines": {
@ -1434,34 +1434,15 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1471,32 +1452,15 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -1513,9 +1477,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"dev": true,
"license": "MIT",
"engines": {
@ -1527,16 +1491,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1582,16 +1544,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1606,13 +1568,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -2794,9 +2756,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true,
"license": "MIT",
"engines": {
@ -4238,15 +4200,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

994
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.8.0",
"@docusaurus/preset-classic": "~3.8.0",
"@docusaurus/core": "~3.7.0",
"@docusaurus/preset-classic": "~3.7.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@ -35,7 +35,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.8.0",
"@docusaurus/module-type-aliases": "~3.7.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4",

142
e2e/package-lock.json generated
View File

@ -1709,17 +1709,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -1733,7 +1733,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -1749,16 +1749,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4"
},
"engines": {
@ -1773,34 +1773,15 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1810,32 +1791,15 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -1852,9 +1816,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"dev": true,
"license": "MIT",
"engines": {
@ -1866,16 +1830,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1921,16 +1883,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1945,13 +1907,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -3872,9 +3834,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true,
"license": "MIT",
"engines": {
@ -6817,15 +6779,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -463,6 +463,8 @@
"assets_count": "{count, plural, one {# asset} other {# assets}}",
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
"assets_downloaded_failed": "{count, plural, one {Downloaded # file - {error} file failed} other {Downloaded # files - {error} files failed}}",
"assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}",
"assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash",
"assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
"assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}",
@ -694,6 +696,7 @@
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark",
"darkTheme": "Toggle dark theme",
"date_after": "Date after",
"date_and_time": "Date and Time",
"date_before": "Date before",
@ -1169,7 +1172,7 @@
"look": "Look",
"loop_videos": "Loop videos",
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
"main_branch_warning": "Youre using a development version; we strongly recommend using a release version!",
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
"main_menu": "Main menu",
"make": "Make",
"manage_shared_links": "Manage shared links",
@ -1434,7 +1437,7 @@
"purchase_lifetime_description": "Lifetime purchase",
"purchase_option_title": "PURCHASE OPTIONS",
"purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.",
"purchase_panel_info_2": "As were committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immichs ongoing development.",
"purchase_panel_info_2": "As we're committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich's ongoing development.",
"purchase_panel_title": "Support the project",
"purchase_per_server": "Per server",
"purchase_per_user": "Per user",
@ -1817,7 +1820,6 @@
"to_parent": "Go to parent",
"to_trash": "Trash",
"toggle_settings": "Toggle settings",
"toggle_theme": "Toggle dark theme",
"total": "Total",
"total_usage": "Total usage",
"trash": "Trash",
@ -1839,6 +1841,7 @@
"unable_to_setup_pin_code": "Unable to setup PIN code",
"unarchive": "Unarchive",
"unarchived_count": "{count, plural, other {Unarchived #}}",
"undo": "Undo",
"unfavorite": "Unfavorite",
"unhide_person": "Unhide person",
"unknown": "Unknown",

View File

@ -19,6 +19,7 @@ class Album {
required this.name,
required this.createdAt,
required this.modifiedAt,
this.description,
this.startDate,
this.endDate,
this.lastModifiedAssetTimestamp,
@ -34,6 +35,7 @@ class Album {
@Index(unique: false, replace: false, type: IndexType.hash)
String? localId;
String name;
String? description;
DateTime createdAt;
DateTime modifiedAt;
DateTime? startDate;
@ -108,6 +110,7 @@ class Album {
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
description == other.description &&
createdAt.isAtSameMomentAs(other.createdAt) &&
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
isAtSameMomentAs(startDate, other.startDate) &&
@ -135,6 +138,7 @@ class Album {
modifiedAt.hashCode ^
startDate.hashCode ^
endDate.hashCode ^
description.hashCode ^
lastModifiedAssetTimestamp.hashCode ^
shared.hashCode ^
activityEnabled.hashCode ^
@ -150,6 +154,7 @@ class Album {
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
description: dto.description,
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
@ -184,7 +189,8 @@ class Album {
}
@override
String toString() => name;
String toString() =>
'remoteId: $remoteId name: $name description: $description';
}
extension AssetsHelper on IsarCollection<Album> {

View File

@ -27,49 +27,54 @@ const AlbumSchema = CollectionSchema(
name: r'createdAt',
type: IsarType.dateTime,
),
r'endDate': PropertySchema(
r'description': PropertySchema(
id: 2,
name: r'description',
type: IsarType.string,
),
r'endDate': PropertySchema(
id: 3,
name: r'endDate',
type: IsarType.dateTime,
),
r'lastModifiedAssetTimestamp': PropertySchema(
id: 3,
id: 4,
name: r'lastModifiedAssetTimestamp',
type: IsarType.dateTime,
),
r'localId': PropertySchema(
id: 4,
id: 5,
name: r'localId',
type: IsarType.string,
),
r'modifiedAt': PropertySchema(
id: 5,
id: 6,
name: r'modifiedAt',
type: IsarType.dateTime,
),
r'name': PropertySchema(
id: 6,
id: 7,
name: r'name',
type: IsarType.string,
),
r'remoteId': PropertySchema(
id: 7,
id: 8,
name: r'remoteId',
type: IsarType.string,
),
r'shared': PropertySchema(
id: 8,
id: 9,
name: r'shared',
type: IsarType.bool,
),
r'sortOrder': PropertySchema(
id: 9,
id: 10,
name: r'sortOrder',
type: IsarType.byte,
enumMap: _AlbumsortOrderEnumValueMap,
),
r'startDate': PropertySchema(
id: 10,
id: 11,
name: r'startDate',
type: IsarType.dateTime,
)
@ -146,6 +151,12 @@ int _albumEstimateSize(
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.description;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.localId;
if (value != null) {
@ -170,15 +181,16 @@ void _albumSerialize(
) {
writer.writeBool(offsets[0], object.activityEnabled);
writer.writeDateTime(offsets[1], object.createdAt);
writer.writeDateTime(offsets[2], object.endDate);
writer.writeDateTime(offsets[3], object.lastModifiedAssetTimestamp);
writer.writeString(offsets[4], object.localId);
writer.writeDateTime(offsets[5], object.modifiedAt);
writer.writeString(offsets[6], object.name);
writer.writeString(offsets[7], object.remoteId);
writer.writeBool(offsets[8], object.shared);
writer.writeByte(offsets[9], object.sortOrder.index);
writer.writeDateTime(offsets[10], object.startDate);
writer.writeString(offsets[2], object.description);
writer.writeDateTime(offsets[3], object.endDate);
writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp);
writer.writeString(offsets[5], object.localId);
writer.writeDateTime(offsets[6], object.modifiedAt);
writer.writeString(offsets[7], object.name);
writer.writeString(offsets[8], object.remoteId);
writer.writeBool(offsets[9], object.shared);
writer.writeByte(offsets[10], object.sortOrder.index);
writer.writeDateTime(offsets[11], object.startDate);
}
Album _albumDeserialize(
@ -190,16 +202,18 @@ Album _albumDeserialize(
final object = Album(
activityEnabled: reader.readBool(offsets[0]),
createdAt: reader.readDateTime(offsets[1]),
endDate: reader.readDateTimeOrNull(offsets[2]),
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[3]),
localId: reader.readStringOrNull(offsets[4]),
modifiedAt: reader.readDateTime(offsets[5]),
name: reader.readString(offsets[6]),
remoteId: reader.readStringOrNull(offsets[7]),
shared: reader.readBool(offsets[8]),
sortOrder: _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[9])] ??
description: reader.readStringOrNull(offsets[2]),
endDate: reader.readDateTimeOrNull(offsets[3]),
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]),
localId: reader.readStringOrNull(offsets[5]),
modifiedAt: reader.readDateTime(offsets[6]),
name: reader.readString(offsets[7]),
remoteId: reader.readStringOrNull(offsets[8]),
shared: reader.readBool(offsets[9]),
sortOrder:
_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ??
SortOrder.desc,
startDate: reader.readDateTimeOrNull(offsets[10]),
startDate: reader.readDateTimeOrNull(offsets[11]),
);
object.id = id;
return object;
@ -217,23 +231,25 @@ P _albumDeserializeProp<P>(
case 1:
return (reader.readDateTime(offset)) as P;
case 2:
return (reader.readDateTimeOrNull(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 3:
return (reader.readDateTimeOrNull(offset)) as P;
case 4:
return (reader.readStringOrNull(offset)) as P;
return (reader.readDateTimeOrNull(offset)) as P;
case 5:
return (reader.readDateTime(offset)) as P;
case 6:
return (reader.readString(offset)) as P;
case 7:
return (reader.readStringOrNull(offset)) as P;
case 6:
return (reader.readDateTime(offset)) as P;
case 7:
return (reader.readString(offset)) as P;
case 8:
return (reader.readBool(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 9:
return (reader.readBool(offset)) as P;
case 10:
return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ??
SortOrder.desc) as P;
case 10:
case 11:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -535,6 +551,152 @@ extension AlbumQueryFilter on QueryBuilder<Album, Album, QFilterCondition> {
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'description',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'description',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'description',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'description',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'description',
value: '',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'description',
value: '',
));
});
}
QueryBuilder<Album, Album, QAfterFilterCondition> endDateIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -1502,6 +1664,18 @@ extension AlbumQuerySortBy on QueryBuilder<Album, Album, QSortBy> {
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByDescription() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.asc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByDescriptionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.desc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> sortByEndDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'endDate', Sort.asc);
@ -1637,6 +1811,18 @@ extension AlbumQuerySortThenBy on QueryBuilder<Album, Album, QSortThenBy> {
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByDescription() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.asc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByDescriptionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.desc);
});
}
QueryBuilder<Album, Album, QAfterSortBy> thenByEndDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'endDate', Sort.asc);
@ -1772,6 +1958,13 @@ extension AlbumQueryWhereDistinct on QueryBuilder<Album, Album, QDistinct> {
});
}
QueryBuilder<Album, Album, QDistinct> distinctByDescription(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'description', caseSensitive: caseSensitive);
});
}
QueryBuilder<Album, Album, QDistinct> distinctByEndDate() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'endDate');
@ -1849,6 +2042,12 @@ extension AlbumQueryProperty on QueryBuilder<Album, Album, QQueryProperty> {
});
}
QueryBuilder<Album, String?, QQueryOperations> descriptionProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'description');
});
}
QueryBuilder<Album, DateTime?, QQueryOperations> endDateProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'endDate');

View File

@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class DriftSyncStreamRepository extends DriftDatabaseRepository
@ -231,22 +232,22 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
});
}
extension on SyncAssetV1TypeEnum {
extension on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
SyncAssetV1TypeEnum.IMAGE => AssetType.image,
SyncAssetV1TypeEnum.VIDEO => AssetType.video,
SyncAssetV1TypeEnum.AUDIO => AssetType.audio,
SyncAssetV1TypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown SyncAssetV1TypeEnum value: $this'),
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}
extension on SyncAssetV1VisibilityEnum {
extension on api.AssetVisibility {
AssetVisibility toAssetVisibility() => switch (this) {
SyncAssetV1VisibilityEnum.timeline => AssetVisibility.timeline,
SyncAssetV1VisibilityEnum.hidden => AssetVisibility.hidden,
SyncAssetV1VisibilityEnum.archive => AssetVisibility.archive,
SyncAssetV1VisibilityEnum.locked => AssetVisibility.locked,
_ => throw Exception('Unknown SyncAssetV1VisibilityEnum value: $this'),
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => throw Exception('Unknown AssetVisibility value: $this'),
};
}

View File

@ -7,7 +7,8 @@ abstract interface class IDownloadRepository {
void Function(TaskProgressUpdate)? onTaskProgress;
Future<List<TaskRecord>> getLiveVideoTasks();
Future<bool> download(DownloadTask task);
Future<List<bool>> downloadAll(List<DownloadTask> tasks);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);

View File

@ -3,18 +3,23 @@ import 'dart:convert';
class AlbumViewerPageState {
final bool isEditAlbum;
final String editTitleText;
final String editDescriptionText;
AlbumViewerPageState({
required this.isEditAlbum,
required this.editTitleText,
required this.editDescriptionText,
});
AlbumViewerPageState copyWith({
bool? isEditAlbum,
String? editTitleText,
String? editDescriptionText,
}) {
return AlbumViewerPageState(
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
editTitleText: editTitleText ?? this.editTitleText,
editDescriptionText: editDescriptionText ?? this.editDescriptionText,
);
}
@ -23,6 +28,7 @@ class AlbumViewerPageState {
result.addAll({'isEditAlbum': isEditAlbum});
result.addAll({'editTitleText': editTitleText});
result.addAll({'editDescriptionText': editDescriptionText});
return result;
}
@ -31,6 +37,7 @@ class AlbumViewerPageState {
return AlbumViewerPageState(
isEditAlbum: map['isEditAlbum'] ?? false,
editTitleText: map['editTitleText'] ?? '',
editDescriptionText: map['editDescriptionText'] ?? '',
);
}
@ -41,7 +48,7 @@ class AlbumViewerPageState {
@override
String toString() =>
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)';
@override
bool operator ==(Object other) {
@ -49,9 +56,13 @@ class AlbumViewerPageState {
return other is AlbumViewerPageState &&
other.isEditAlbum == isEditAlbum &&
other.editTitleText == editTitleText;
other.editTitleText == editTitleText &&
other.editDescriptionText == editDescriptionText;
}
@override
int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode;
int get hashCode =>
isEditAlbum.hashCode ^
editTitleText.hashCode ^
editDescriptionText.hashCode;
}

View File

@ -35,7 +35,7 @@ class AssetSelectionState {
@override
String toString() =>
'SelectionAssetState(hasRemote: $hasRemote, hasMerged: $hasMerged, hasMerged: $hasMerged, selectedCount: $selectedCount)';
'SelectionAssetState(hasRemote: $hasRemote, hasLocal: $hasLocal, hasMerged: $hasMerged, selectedCount: $selectedCount)';
@override
bool operator ==(covariant AssetSelectionState other) {

View File

@ -26,9 +26,9 @@ class AlbumControlButton extends ConsumerWidget {
);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
padding: const EdgeInsets.only(left: 16.0),
child: SizedBox(
height: 40,
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
children: [

View File

@ -30,15 +30,12 @@ class AlbumDateRange extends ConsumerWidget {
final (startDate, endDate, shared) = data;
return Padding(
padding: shared
? const EdgeInsets.only(
left: 16.0,
bottom: 0.0,
)
: const EdgeInsets.only(left: 16.0, bottom: 8.0),
padding: const EdgeInsets.only(left: 16.0),
child: Text(
_getDateRangeText(startDate, endDate),
style: context.textTheme.labelLarge,
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
);
}

View File

@ -0,0 +1,45 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
class AlbumDescription extends ConsumerWidget {
const AlbumDescription({super.key, required this.descriptionFocusNode});
final FocusNode descriptionFocusNode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final userId = ref.watch(authProvider).userId;
final (isOwner, isRemote, albumDescription) = ref.watch(
currentAlbumProvider.select((album) {
if (album == null) {
return const (false, false, '');
}
return (album.ownerId == userId, album.isRemote, album.description);
}),
);
if (isOwner && isRemote) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: AlbumViewerEditableDescription(
albumDescription: albumDescription ?? 'add_a_description'.tr(),
descriptionFocusNode: descriptionFocusNode,
),
);
}
return Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(
albumDescription ?? 'add_a_description'.tr(),
style: context.textTheme.bodyLarge,
),
);
}
}

View File

@ -36,7 +36,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
child: SizedBox(
height: 50,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
padding: const EdgeInsets.only(left: 16, bottom: 8),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(

View File

@ -19,7 +19,11 @@ class AlbumTitle extends ConsumerWidget {
return const (false, false, '');
}
return (album.ownerId == userId, album.isRemote, album.name);
return (
album.ownerId == userId,
album.isRemote,
album.name,
);
}),
);
@ -35,7 +39,12 @@ class AlbumTitle extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(albumName, style: context.textTheme.headlineMedium),
child: Text(
albumName,
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
);
}
}

View File

@ -10,6 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/pages/album/album_control_button.dart';
import 'package:immich_mobile/pages/album/album_date_range.dart';
import 'package:immich_mobile/pages/album/album_description.dart';
import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
import 'package:immich_mobile/pages/album/album_title.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
@ -36,6 +37,7 @@ class AlbumViewer extends HookConsumerWidget {
}
final titleFocusNode = useFocusNode();
final descriptionFocusNode = useFocusNode();
final userId = ref.watch(authProvider).userId;
final isMultiselecting = ref.watch(multiselectProvider);
final isProcessing = useProcessingOverlay();
@ -106,15 +108,34 @@ class AlbumViewer extends HookConsumerWidget {
MultiselectGrid(
key: const ValueKey("albumViewerMultiselectGrid"),
renderListProvider: albumTimelineProvider(album.id),
topWidget: Column(
topWidget: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
context.primaryColor.withValues(alpha: 0.04),
context.primaryColor.withValues(alpha: 0.02),
Colors.orange.withValues(alpha: 0.02),
Colors.transparent,
],
stops: const [0.0, 0.3, 0.7, 1.0],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
const AlbumDateRange(),
AlbumTitle(
key: const ValueKey("albumTitle"),
titleFocusNode: titleFocusNode,
),
const AlbumDateRange(),
AlbumDescription(
key: const ValueKey("albumDescription"),
descriptionFocusNode: descriptionFocusNode,
),
const AlbumSharedUserIcons(),
if (album.isRemote)
AlbumControlButton(
@ -122,8 +143,10 @@ class AlbumViewer extends HookConsumerWidget {
onAddPhotosPressed: onAddPhotosPressed,
onAddUsersPressed: onAddUsersPressed,
),
const SizedBox(height: 8),
],
),
),
onRemoveFromAlbum: onRemoveFromAlbumPressed,
editEnabled: album.ownerId == userId,
),
@ -136,6 +159,7 @@ class AlbumViewer extends HookConsumerWidget {
child: AlbumViewerAppbar(
key: const ValueKey("albumViewerAppbar"),
titleFocusNode: titleFocusNode,
descriptionFocusNode: descriptionFocusNode,
userId: userId,
onAddPhotos: onAddPhotosPressed,
onAddUsers: onAddUsersPressed,

View File

@ -8,9 +8,11 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
@RoutePage()
@ -28,6 +30,7 @@ class CreateAlbumPage extends HookConsumerWidget {
final albumTitleController =
useTextEditingController.fromValue(TextEditingValue.empty);
final albumTitleTextFieldFocusNode = useFocusNode();
final albumDescriptionTextFieldFocusNode = useFocusNode();
final isAlbumTitleTextFieldFocus = useState(false);
final isAlbumTitleEmpty = useState(true);
final selectedAssets = useState<Set<Asset>>(
@ -36,6 +39,7 @@ class CreateAlbumPage extends HookConsumerWidget {
void onBackgroundTapped() {
albumTitleTextFieldFocusNode.unfocus();
albumDescriptionTextFieldFocusNode.unfocus();
isAlbumTitleTextFieldFocus.value = false;
if (albumTitleController.text.isEmpty) {
@ -77,6 +81,19 @@ class CreateAlbumPage extends HookConsumerWidget {
);
}
buildDescriptionInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10,
left: 10,
),
child: AlbumViewerEditableDescription(
albumDescription: '',
descriptionFocusNode: albumDescriptionTextFieldFocusNode,
),
);
}
buildTitle() {
if (selectedAssets.value.isEmpty) {
return SliverToBoxAdapter(
@ -178,18 +195,18 @@ class CreateAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
createNonSharedAlbum() async {
Future<void> createAlbum() async {
onBackgroundTapped();
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
ref.read(albumTitleProvider),
selectedAssets.value,
);
if (newAlbum != null) {
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
ref.read(albumProvider.notifier).refreshRemoteAlbums();
selectedAssets.value = {};
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
ref.read(albumTitleProvider.notifier).clearAlbumTitle();
ref.read(albumViewerProvider.notifier).disableEditAlbum();
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
}
}
@ -211,9 +228,8 @@ class CreateAlbumPage extends HookConsumerWidget {
).tr(),
actions: [
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? createNonSharedAlbum
: null,
onPressed:
albumTitleController.text.isNotEmpty ? createAlbum : null,
child: Text(
'create'.tr(),
style: TextStyle(
@ -237,10 +253,11 @@ class CreateAlbumPage extends HookConsumerWidget {
pinned: true,
floating: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(96.0),
preferredSize: const Size.fromHeight(125.0),
child: Column(
children: [
buildTitleInputField(),
buildDescriptionInputField(),
if (selectedAssets.value.isNotEmpty) buildControlButton(),
],
),

View File

@ -72,7 +72,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
return;
}
if (context.router.current.name != ShareIntentRoute.name) {
context.replaceRoute(const TabControllerRoute());
}
final hasPermission =
await ref.read(galleryPermissionNotifier.notifier).hasPermission;

View File

@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/url_helper.dart';
@RoutePage()
@ -20,6 +21,11 @@ class ShareIntentPage extends HookConsumerWidget {
final currentEndpoint = getServerUrl() ?? '--';
final candidates = ref.watch(shareIntentUploadProvider);
final isUploaded = useState(false);
useOnAppLifecycleStateChange((previous, current) {
if (current == AppLifecycleState.resumed) {
isUploaded.value = false;
}
});
void removeAttachment(ShareIntentAttachment attachment) {
ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment);
@ -66,6 +72,14 @@ class ShareIntentPage extends HookConsumerWidget {
),
],
),
leading: IconButton(
onPressed: () {
context.navigateTo(
const TabControllerRoute(),
);
},
icon: const Icon(Icons.arrow_back),
),
),
body: ListView.builder(
itemCount: attachments.length,

View File

@ -5,7 +5,13 @@ import 'package:immich_mobile/entities/album.entity.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref)
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
: super(
AlbumViewerPageState(
editTitleText: "",
isEditAlbum: false,
editDescriptionText: "",
),
);
final Ref ref;
@ -21,12 +27,24 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
state = state.copyWith(editTitleText: newTitle);
}
void setEditDescriptionText(String newDescription) {
state = state.copyWith(editDescriptionText: newDescription);
}
void remoteEditTitleText() {
state = state.copyWith(editTitleText: "");
}
void remoteEditDescriptionText() {
state = state.copyWith(editDescriptionText: "");
}
void resetState() {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
state = state.copyWith(
editTitleText: "",
isEditAlbum: false,
editDescriptionText: "",
);
}
Future<bool> changeAlbumTitle(
@ -46,6 +64,28 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
return false;
}
Future<bool> changeAlbumDescription(
Album album,
String newAlbumDescription,
) async {
AlbumService service = ref.watch(albumServiceProvider);
bool isSuccess = await service.changeDescriptionAlbum(
album,
newAlbumDescription,
);
if (isSuccess) {
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
return true;
}
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
return false;
}
}
final albumViewerProvider =

View File

@ -140,6 +140,10 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
});
}
Future<List<bool>> downloadAllAsset(List<Asset> assets) async {
return await _downloadService.downloadAll(assets);
}
void downloadAsset(Asset asset, BuildContext context) async {
await _downloadService.download(asset);
}

View File

@ -36,6 +36,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
String? description,
}) async {
final users = sharedUserIds.map(
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
@ -44,6 +45,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
_api.createAlbum(
CreateAlbumDto(
albumName: name,
description: description,
assetIds: assetIds.toList(),
albumUsers: users.toList(),
),
@ -161,6 +163,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
description: dto.description,
endDate: dto.endDate,
activityEnabled: dto.isActivityEnabled,
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
@ -174,6 +177,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
album.sharedUsers.addAll(users.map(entity.User.fromDto));
final assets = dto.assets.map(Asset.remote).toList();
album.assets.addAll(assets);
return album;
}
}

View File

@ -39,8 +39,8 @@ class DownloadRepository implements IDownloadRepository {
}
@override
Future<bool> download(DownloadTask task) {
return FileDownloader().enqueue(task);
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
return FileDownloader().enqueueAll(tasks);
}
@override

View File

@ -422,6 +422,25 @@ class AlbumService {
}
}
Future<bool> changeDescriptionAlbum(
Album album,
String newAlbumDescription,
) async {
try {
final updatedAlbum = await _albumApiRepository.update(
album.remoteId!,
description: newAlbumDescription,
);
album.description = updatedAlbum.description;
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeDescriptionAlbum ${e.toString()}");
return false;
}
}
Future<Album?> getAlbumByName(
String name, {
bool? remote,

View File

@ -159,9 +159,19 @@ class DownloadService {
return await FileDownloader().cancelTaskWithId(id);
}
Future<List<bool>> downloadAll(List<Asset> assets) async {
return await _downloadRepository
.downloadAll(assets.expand(_createDownloadTasks).toList());
}
Future<void> download(Asset asset) async {
final tasks = _createDownloadTasks(asset);
await _downloadRepository.downloadAll(tasks);
}
List<DownloadTask> _createDownloadTasks(Asset asset) {
if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
await _downloadRepository.download(
return [
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
@ -171,9 +181,6 @@ class DownloadService {
id: asset.remoteId!,
).toJson(),
),
);
await _downloadRepository.download(
_buildDownloadTask(
asset.livePhotoVideoId!,
asset.fileName
@ -185,16 +192,20 @@ class DownloadService {
id: asset.remoteId!,
).toJson(),
),
);
} else {
await _downloadRepository.download(
];
}
if (asset.remoteId == null) {
return [];
}
return [
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
group: asset.isImage ? downloadGroupImage : downloadGroupVideo,
),
);
}
];
}
DownloadTask _buildDownloadTask(

View File

@ -451,6 +451,7 @@ class SyncService {
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
album.name = dto.name;
album.description = dto.description;
album.shared = dto.shared;
album.createdAt = dto.createdAt;
album.modifiedAt = dto.modifiedAt;
@ -643,6 +644,7 @@ class SyncService {
toUpdate.isEmpty &&
toDelete.isEmpty &&
dbAlbum.name == deviceAlbum.name &&
dbAlbum.description == deviceAlbum.description &&
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
// changes only affeted excluded albums
_log.info(
@ -670,6 +672,7 @@ class SyncService {
deleteCandidates.addAll(toDelete);
existing.addAll(existingInDb);
dbAlbum.name = deviceAlbum.name;
dbAlbum.description = deviceAlbum.description;
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
if (dbAlbum.thumbnail.value != null &&
toDelete.contains(dbAlbum.thumbnail.value)) {
@ -943,6 +946,7 @@ class SyncService {
Album dbAlbum,
) async {
return deviceAlbum.name != dbAlbum.name ||
deviceAlbum.description != dbAlbum.description ||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
@ -1101,6 +1105,7 @@ class SyncService {
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
remoteAlbum.name != dbAlbum.name ||
remoteAlbum.description != dbAlbum.description ||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
remoteAlbum.shared != dbAlbum.shared ||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||

View File

@ -16,7 +16,7 @@ class AlbumActionFilledButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 16.0),
padding: const EdgeInsets.only(right: 8.0),
child: FilledButton.icon(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
@ -32,9 +32,7 @@ class AlbumActionFilledButton extends StatelessWidget {
),
label: Text(
labelText,
style: context.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.labelLarge?.copyWith(),
),
onPressed: onPressed,
),

View File

@ -18,6 +18,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
super.key,
required this.userId,
required this.titleFocusNode,
required this.descriptionFocusNode,
this.onAddPhotos,
this.onAddUsers,
required this.onActivities,
@ -25,6 +26,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
final String userId;
final FocusNode titleFocusNode;
final FocusNode descriptionFocusNode;
final void Function()? onAddPhotos;
final void Function()? onAddUsers;
final void Function() onActivities;
@ -48,6 +50,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
final albumViewer = ref.watch(albumViewerProvider);
final newAlbumTitle = albumViewer.editTitleText;
final newAlbumDescription = albumViewer.editDescriptionText;
final isEditAlbum = albumViewer.isEditAlbum;
final comments = album.shared
@ -277,10 +280,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
if (isEditAlbum) {
return IconButton(
onPressed: () async {
if (newAlbumTitle.isNotEmpty) {
bool isSuccess = await ref
.watch(albumViewerProvider.notifier)
.changeAlbumTitle(album, newAlbumTitle);
if (!isSuccess) {
ImmichToast.show(
context: context,
@ -289,8 +292,25 @@ class AlbumViewerAppbar extends HookConsumerWidget
toastType: ToastType.error,
);
}
titleFocusNode.unfocus();
} else if (newAlbumDescription.isNotEmpty) {
bool isSuccessDescription = await ref
.watch(albumViewerProvider.notifier)
.changeAlbumDescription(album, newAlbumDescription);
if (!isSuccessDescription) {
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_description".tr(),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
descriptionFocusNode.unfocus();
} else {
titleFocusNode.unfocus();
descriptionFocusNode.unfocus();
ref.read(albumViewerProvider.notifier).disableEditAlbum();
}
},
icon: const Icon(Icons.check_rounded),
splashRadius: 25,

View File

@ -0,0 +1,102 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
class AlbumViewerEditableDescription extends HookConsumerWidget {
final String albumDescription;
final FocusNode descriptionFocusNode;
const AlbumViewerEditableDescription({
super.key,
required this.albumDescription,
required this.descriptionFocusNode,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumViewerState = ref.watch(albumViewerProvider);
final descriptionTextEditController = useTextEditingController(
text: albumViewerState.isEditAlbum &&
albumViewerState.editDescriptionText.isNotEmpty
? albumViewerState.editDescriptionText
: albumDescription,
);
void onFocusModeChange() {
if (!descriptionFocusNode.hasFocus &&
descriptionTextEditController.text.isEmpty) {
ref.watch(albumViewerProvider.notifier).setEditDescriptionText("");
descriptionTextEditController.text = "";
}
}
useEffect(
() {
descriptionFocusNode.addListener(onFocusModeChange);
return () {
descriptionFocusNode.removeListener(onFocusModeChange);
};
},
[],
);
return Material(
color: Colors.transparent,
child: TextField(
onChanged: (value) {
if (value.isEmpty) {
} else {
ref
.watch(albumViewerProvider.notifier)
.setEditDescriptionText(value);
}
},
focusNode: descriptionFocusNode,
style: context.textTheme.bodyMedium,
maxLines: 3,
minLines: 1,
controller: descriptionTextEditController,
onTap: () {
context.focusScope.requestFocus(descriptionFocusNode);
ref
.watch(albumViewerProvider.notifier)
.setEditDescriptionText(albumDescription);
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
if (descriptionTextEditController.text == '') {
descriptionTextEditController.clear();
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8),
suffixIcon: descriptionFocusNode.hasFocus
? IconButton(
onPressed: () {
descriptionTextEditController.clear();
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
focusColor: Colors.grey[300],
fillColor: context.scaffoldBackgroundColor,
filled: descriptionFocusNode.hasFocus,
hintText: 'add_a_description'.tr(),
),
),
);
}
}

View File

@ -52,7 +52,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
},
focusNode: titleFocusNode,
style: context.textTheme.headlineMedium,
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w700,
),
controller: titleTextEditController,
onTap: () {
context.focusScope.requestFocus(titleFocusNode);
@ -65,8 +67,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}
},
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
suffixIcon: titleFocusNode.hasFocus
? IconButton(
onPressed: () {

View File

@ -39,6 +39,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
final void Function()? onEditLocation;
final void Function()? onRemoveFromAlbum;
final void Function()? onToggleLocked;
final void Function()? onDownload;
final bool enabled;
final bool unfavorite;
@ -56,6 +57,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
required this.onAddToAlbum,
required this.onCreateNewAlbum,
required this.onUpload,
this.onDownload,
this.onStack,
this.onEditTime,
this.onEditLocation,
@ -158,6 +160,15 @@ class ControlBottomAppBar extends HookConsumerWidget {
label: (unfavorite ? "unfavorite" : "favorite").tr(),
onPressed: enabled ? onFavorite : null,
),
if (hasRemote && onDownload != null)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 90),
child: ControlBoxButton(
iconData: Icons.download,
label: "download".tr(),
onPressed: onDownload,
),
),
if (hasLocal && hasRemote && onDelete != null && !isInLockedView)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 90),

View File

@ -14,6 +14,7 @@ import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/models/asset_selection_state.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
@ -23,6 +24,7 @@ import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/stack.service.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/utils/selection_handlers.dart';
import 'package:immich_mobile/utils/translation.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
@ -44,6 +46,7 @@ class MultiselectGrid extends HookConsumerWidget {
this.editEnabled = false,
this.unarchive = false,
this.unfavorite = false,
this.downloadEnabled = true,
this.emptyIndicator,
});
@ -57,6 +60,7 @@ class MultiselectGrid extends HookConsumerWidget {
final bool archiveEnabled;
final bool unarchive;
final bool deleteEnabled;
final bool downloadEnabled;
final bool favoriteEnabled;
final bool unfavorite;
final bool editEnabled;
@ -239,6 +243,39 @@ class MultiselectGrid extends HookConsumerWidget {
}
}
void onDownload() async {
processing.value = true;
try {
final toDownload = selection.value.toList();
final results = await ref
.read(downloadStateProvider.notifier)
.downloadAllAsset(toDownload);
final totalCount = toDownload.length;
final successCount = results.where((e) => e).length;
final failedCount = totalCount - successCount;
final msg = failedCount > 0
? t('assets_downloaded_failed', {
'count': successCount,
'error': failedCount,
})
: t('assets_downloaded_successfully', {
'count': successCount,
});
ImmichToast.show(
context: context,
msg: msg,
gravity: ToastGravity.BOTTOM,
);
} finally {
processing.value = false;
selectionEnabledHook.value = false;
}
}
void onDeleteRemote([bool shouldDeletePermanently = false]) async {
processing.value = true;
try {
@ -474,6 +511,7 @@ class MultiselectGrid extends HookConsumerWidget {
onArchive: archiveEnabled ? onArchiveAsset : null,
onDelete: deleteEnabled ? onDelete : null,
onDeleteServer: deleteEnabled ? onDeleteRemote : null,
onDownload: downloadEnabled ? onDownload : null,
/// local file deletion is allowed irrespective of [deleteEnabled] since it has
/// nothing to do with the state of the asset in the Immich server

View File

@ -31,10 +31,6 @@ class TimelineApi {
///
/// * [AssetOrder] order:
///
/// * [num] page:
///
/// * [num] pageSize:
///
/// * [String] personId:
///
/// * [String] tagId:
@ -46,7 +42,7 @@ class TimelineApi {
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, num? page, num? pageSize, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/bucket';
@ -72,12 +68,6 @@ class TimelineApi {
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (page != null) {
queryParams.addAll(_queryParams('', 'page', page));
}
if (pageSize != null) {
queryParams.addAll(_queryParams('', 'pageSize', pageSize));
}
if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId));
}
@ -126,10 +116,6 @@ class TimelineApi {
///
/// * [AssetOrder] order:
///
/// * [num] page:
///
/// * [num] pageSize:
///
/// * [String] personId:
///
/// * [String] tagId:
@ -141,8 +127,8 @@ class TimelineApi {
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, num? page, num? pageSize, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, page: page, pageSize: pageSize, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@ -14,25 +14,31 @@ class ActivityStatisticsResponseDto {
/// Returns a new [ActivityStatisticsResponseDto] instance.
ActivityStatisticsResponseDto({
required this.comments,
required this.likes,
});
int comments;
int likes;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityStatisticsResponseDto &&
other.comments == comments;
other.comments == comments &&
other.likes == likes;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(comments.hashCode);
(comments.hashCode) +
(likes.hashCode);
@override
String toString() => 'ActivityStatisticsResponseDto[comments=$comments]';
String toString() => 'ActivityStatisticsResponseDto[comments=$comments, likes=$likes]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'comments'] = this.comments;
json[r'likes'] = this.likes;
return json;
}
@ -46,6 +52,7 @@ class ActivityStatisticsResponseDto {
return ActivityStatisticsResponseDto(
comments: mapValueOfType<int>(json, r'comments')!,
likes: mapValueOfType<int>(json, r'likes')!,
);
}
return null;
@ -94,6 +101,7 @@ class ActivityStatisticsResponseDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'comments',
'likes',
};
}

View File

@ -20,7 +20,7 @@ class SyncAlbumUserV1 {
String albumId;
SyncAlbumUserV1RoleEnum role;
AlbumUserRole role;
String userId;
@ -58,7 +58,7 @@ class SyncAlbumUserV1 {
return SyncAlbumUserV1(
albumId: mapValueOfType<String>(json, r'albumId')!,
role: SyncAlbumUserV1RoleEnum.fromJson(json[r'role'])!,
role: AlbumUserRole.fromJson(json[r'role'])!,
userId: mapValueOfType<String>(json, r'userId')!,
);
}
@ -113,77 +113,3 @@ class SyncAlbumUserV1 {
};
}
class SyncAlbumUserV1RoleEnum {
/// Instantiate a new enum with the provided [value].
const SyncAlbumUserV1RoleEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const editor = SyncAlbumUserV1RoleEnum._(r'editor');
static const viewer = SyncAlbumUserV1RoleEnum._(r'viewer');
/// List of all possible values in this [enum][SyncAlbumUserV1RoleEnum].
static const values = <SyncAlbumUserV1RoleEnum>[
editor,
viewer,
];
static SyncAlbumUserV1RoleEnum? fromJson(dynamic value) => SyncAlbumUserV1RoleEnumTypeTransformer().decode(value);
static List<SyncAlbumUserV1RoleEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAlbumUserV1RoleEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAlbumUserV1RoleEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncAlbumUserV1RoleEnum] to String,
/// and [decode] dynamic data back to [SyncAlbumUserV1RoleEnum].
class SyncAlbumUserV1RoleEnumTypeTransformer {
factory SyncAlbumUserV1RoleEnumTypeTransformer() => _instance ??= const SyncAlbumUserV1RoleEnumTypeTransformer._();
const SyncAlbumUserV1RoleEnumTypeTransformer._();
String encode(SyncAlbumUserV1RoleEnum data) => data.value;
/// Decodes a [dynamic value][data] to a SyncAlbumUserV1RoleEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
SyncAlbumUserV1RoleEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'editor': return SyncAlbumUserV1RoleEnum.editor;
case r'viewer': return SyncAlbumUserV1RoleEnum.viewer;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncAlbumUserV1RoleEnumTypeTransformer] instance.
static SyncAlbumUserV1RoleEnumTypeTransformer? _instance;
}

View File

@ -47,9 +47,9 @@ class SyncAssetV1 {
String? thumbhash;
SyncAssetV1TypeEnum type;
AssetTypeEnum type;
SyncAssetV1VisibilityEnum visibility;
AssetVisibility visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
@ -141,8 +141,8 @@ class SyncAssetV1 {
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
ownerId: mapValueOfType<String>(json, r'ownerId')!,
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!,
visibility: SyncAssetV1VisibilityEnum.fromJson(json[r'visibility'])!,
type: AssetTypeEnum.fromJson(json[r'type'])!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
);
}
return null;
@ -205,163 +205,3 @@ class SyncAssetV1 {
};
}
class SyncAssetV1TypeEnum {
/// Instantiate a new enum with the provided [value].
const SyncAssetV1TypeEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const IMAGE = SyncAssetV1TypeEnum._(r'IMAGE');
static const VIDEO = SyncAssetV1TypeEnum._(r'VIDEO');
static const AUDIO = SyncAssetV1TypeEnum._(r'AUDIO');
static const OTHER = SyncAssetV1TypeEnum._(r'OTHER');
/// List of all possible values in this [enum][SyncAssetV1TypeEnum].
static const values = <SyncAssetV1TypeEnum>[
IMAGE,
VIDEO,
AUDIO,
OTHER,
];
static SyncAssetV1TypeEnum? fromJson(dynamic value) => SyncAssetV1TypeEnumTypeTransformer().decode(value);
static List<SyncAssetV1TypeEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetV1TypeEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetV1TypeEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncAssetV1TypeEnum] to String,
/// and [decode] dynamic data back to [SyncAssetV1TypeEnum].
class SyncAssetV1TypeEnumTypeTransformer {
factory SyncAssetV1TypeEnumTypeTransformer() => _instance ??= const SyncAssetV1TypeEnumTypeTransformer._();
const SyncAssetV1TypeEnumTypeTransformer._();
String encode(SyncAssetV1TypeEnum data) => data.value;
/// Decodes a [dynamic value][data] to a SyncAssetV1TypeEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
SyncAssetV1TypeEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'IMAGE': return SyncAssetV1TypeEnum.IMAGE;
case r'VIDEO': return SyncAssetV1TypeEnum.VIDEO;
case r'AUDIO': return SyncAssetV1TypeEnum.AUDIO;
case r'OTHER': return SyncAssetV1TypeEnum.OTHER;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncAssetV1TypeEnumTypeTransformer] instance.
static SyncAssetV1TypeEnumTypeTransformer? _instance;
}
class SyncAssetV1VisibilityEnum {
/// Instantiate a new enum with the provided [value].
const SyncAssetV1VisibilityEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const archive = SyncAssetV1VisibilityEnum._(r'archive');
static const timeline = SyncAssetV1VisibilityEnum._(r'timeline');
static const hidden = SyncAssetV1VisibilityEnum._(r'hidden');
static const locked = SyncAssetV1VisibilityEnum._(r'locked');
/// List of all possible values in this [enum][SyncAssetV1VisibilityEnum].
static const values = <SyncAssetV1VisibilityEnum>[
archive,
timeline,
hidden,
locked,
];
static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value);
static List<SyncAssetV1VisibilityEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetV1VisibilityEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetV1VisibilityEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncAssetV1VisibilityEnum] to String,
/// and [decode] dynamic data back to [SyncAssetV1VisibilityEnum].
class SyncAssetV1VisibilityEnumTypeTransformer {
factory SyncAssetV1VisibilityEnumTypeTransformer() => _instance ??= const SyncAssetV1VisibilityEnumTypeTransformer._();
const SyncAssetV1VisibilityEnumTypeTransformer._();
String encode(SyncAssetV1VisibilityEnum data) => data.value;
/// Decodes a [dynamic value][data] to a SyncAssetV1VisibilityEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
SyncAssetV1VisibilityEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'archive': return SyncAssetV1VisibilityEnum.archive;
case r'timeline': return SyncAssetV1VisibilityEnum.timeline;
case r'hidden': return SyncAssetV1VisibilityEnum.hidden;
case r'locked': return SyncAssetV1VisibilityEnum.locked;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncAssetV1VisibilityEnumTypeTransformer] instance.
static SyncAssetV1VisibilityEnumTypeTransformer? _instance;
}

View File

@ -7380,24 +7380,6 @@
"$ref": "#/components/schemas/AssetOrder"
}
},
{
"name": "page",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"type": "number"
}
},
{
"name": "pageSize",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"type": "number"
}
},
{
"name": "personId",
"required": false,
@ -8500,10 +8482,14 @@
"properties": {
"comments": {
"type": "integer"
},
"likes": {
"type": "integer"
}
},
"required": [
"comments"
"comments",
"likes"
],
"type": "object"
},
@ -13006,11 +12992,11 @@
"type": "string"
},
"role": {
"enum": [
"editor",
"viewer"
],
"type": "string"
"allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
},
"userId": {
"type": "string"
@ -13259,22 +13245,18 @@
"type": "string"
},
"type": {
"enum": [
"IMAGE",
"VIDEO",
"AUDIO",
"OTHER"
],
"type": "string"
"allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
},
"visibility": {
"enum": [
"archive",
"timeline",
"hidden",
"locked"
],
"type": "string"
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
}
},
"required": [

View File

@ -38,6 +38,7 @@ export type ActivityCreateDto = {
};
export type ActivityStatisticsResponseDto = {
comments: number;
likes: number;
};
export type NotificationCreateDto = {
data?: object;
@ -3435,14 +3436,12 @@ export function tagAssets({ id, bulkIdsDto }: {
body: bulkIdsDto
})));
}
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, page, pageSize, personId, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
albumId?: string;
isFavorite?: boolean;
isTrashed?: boolean;
key?: string;
order?: AssetOrder;
page?: number;
pageSize?: number;
personId?: string;
tagId?: string;
timeBucket: string;
@ -3460,8 +3459,6 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, page
isTrashed,
key,
order,
page,
pageSize,
personId,
tagId,
timeBucket,

214
server/package-lock.json generated
View File

@ -2349,12 +2349,12 @@
}
},
"node_modules/@nestjs/common": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.2.tgz",
"integrity": "sha512-cHh4OPH44PjaHM93D1jgE1HO/B7XTZVRDxy/cPuGgyMEA4p2zXO+qqcOgTMC5FYcp7dX9jLeCjXAU0ToFAnODw==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.1.tgz",
"integrity": "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA==",
"license": "MIT",
"dependencies": {
"file-type": "21.0.0",
"file-type": "20.5.0",
"iterare": "1.2.1",
"load-esm": "1.0.2",
"tslib": "2.8.1",
@ -2380,9 +2380,9 @@
}
},
"node_modules/@nestjs/core": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.2.tgz",
"integrity": "sha512-QRuyxwu0BjNfmmmunsw1ylX7RSyfDQHt+xD+tKncdtgiMOOzAu+LA1gB4WoZnw4frQkk+qZbhEbM61cIjOxD3w==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.1.tgz",
"integrity": "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@ -2454,14 +2454,14 @@
}
},
"node_modules/@nestjs/platform-express": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.2.tgz",
"integrity": "sha512-GlNwOT4htRp8RpZ+TpqGtSHwGKw/abdxxBRse40XE2SWs5ikaoujr9Yd+5sJWDNXB4QTftwb+FplXhyk1Ra+4A==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.1.tgz",
"integrity": "sha512-IUxk380qnUtz0PCRQ5i+o9UHlGMrFzGPIJxDwyt3JZZwx2AngOlcEcm5e+7YeJQEr2QYX2QyC4tUQg0zde+D7A==",
"license": "MIT",
"dependencies": {
"cors": "2.8.5",
"express": "5.1.0",
"multer": "2.0.0",
"multer": "1.4.5-lts.2",
"path-to-regexp": "8.2.0",
"tslib": "2.8.1"
},
@ -2475,9 +2475,9 @@
}
},
"node_modules/@nestjs/platform-socket.io": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.2.tgz",
"integrity": "sha512-IkeDPRRddY0In6lE+5H/DJodtF5cEx+ga+GWehs4Il5Y3kK7MVR2/WgUABAhyRsbJYOhIhZD7Dai0V2t9ref1Q==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.1.tgz",
"integrity": "sha512-Bsc8ouysUFasWiO8RKEvppqYM5LNkHfbyIJQTy3V6+PUdYhblkvmOq8QtjuHpv6DiBI4siUcxACx/90/CdXLkQ==",
"license": "MIT",
"dependencies": {
"socket.io": "4.8.1",
@ -2663,9 +2663,9 @@
}
},
"node_modules/@nestjs/testing": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.2.tgz",
"integrity": "sha512-BQxVKUVW6gzEbbHAvmg5RgcP3s++pRgTCmsgaDF/DtcLRUeKi8SjAdqzLm14xbkMeibxOf3fNqM2iwqUKj8ffw==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.1.tgz",
"integrity": "sha512-stzm8YrLDGAijHYQw+8Z9dD6lGdvahL0hIjGVZ/0KBxLZht0/rvRjgV31UK+DUqXaF7yhJTw9ryrPaITxI1J6A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2691,9 +2691,9 @@
}
},
"node_modules/@nestjs/websockets": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.2.tgz",
"integrity": "sha512-Ywl7u0C3+qnKIrk0mD3jHWnowO+GScFT1FeP6cNgarA0ujHEfusph9IIbnUJiEiusfnKVpK9fYMGZRSDwnRGPQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.1.tgz",
"integrity": "sha512-gxwQoGx5bW5IvparzrX1UOGXz87eqY0fK5Y6yb14z6tSSubQTciNjCDm5osDEkRyRCG6ZB0F+eXF6dRUjwTlBQ==",
"license": "MIT",
"dependencies": {
"iterare": "1.2.1",
@ -5576,9 +5576,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz",
"integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==",
"version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz",
"integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5735,17 +5735,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -5759,7 +5759,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -5775,16 +5775,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4"
},
"engines": {
@ -5799,34 +5799,15 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5836,32 +5817,15 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -5878,9 +5842,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"dev": true,
"license": "MIT",
"engines": {
@ -5892,16 +5856,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -5947,16 +5909,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5971,13 +5933,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -9610,18 +9572,18 @@
}
},
"node_modules/file-type": {
"version": "21.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
"integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
"version": "20.5.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz",
"integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==",
"license": "MIT",
"dependencies": {
"@tokenizer/inflate": "^0.2.7",
"strtok3": "^10.2.2",
"@tokenizer/inflate": "^0.2.6",
"strtok3": "^10.2.0",
"token-types": "^6.0.0",
"uint8array-extras": "^1.4.0"
},
"engines": {
"node": ">=20"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
@ -10239,9 +10201,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true,
"license": "MIT",
"engines": {
@ -12163,9 +12125,9 @@
}
},
"node_modules/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.0.tgz",
"integrity": "sha512-bS8rPZurbAuHGAnApbM9d4h1wSoYqrOqkE+6a64KLMK9yWU7gJXBDDVklKQ3TPi9DRb85cRs6yXaC0+cjxRtRg==",
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@ -12177,7 +12139,7 @@
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 10.16.0"
"node": ">= 6.0.0"
}
},
"node_modules/multer/node_modules/concat-stream": {
@ -17090,15 +17052,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -17417,9 +17379,9 @@
}
},
"node_modules/validator": {
"version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz",
"integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"

View File

@ -29,6 +29,9 @@ export class ActivityResponseDto {
export class ActivityStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
comments!: number;
@ApiProperty({ type: 'integer' })
likes!: number;
}
export class ActivityDto {

View File

@ -65,9 +65,11 @@ export class SyncAssetV1 {
fileCreatedAt!: Date | null;
fileModifiedAt!: Date | null;
localDateTime!: Date | null;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType;
deletedAt!: Date | null;
isFavorite!: boolean;
@ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility })
visibility!: AssetVisibility;
}
@ -125,6 +127,7 @@ export class SyncAlbumUserDeleteV1 {
export class SyncAlbumUserV1 {
albumId!: string;
userId!: string;
@ApiProperty({ enumName: 'AlbumUserRole', enum: AlbumUserRole })
role!: AlbumUserRole;
}

View File

@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsInt, IsString, Min } from 'class-validator';
import { IsEnum, IsString } from 'class-validator';
import { AssetOrder, AssetVisibility } from 'src/enum';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
@ -41,16 +41,6 @@ export class TimeBucketDto {
export class TimeBucketAssetDto extends TimeBucketDto {
@IsString()
timeBucket!: string;
@IsInt()
@Min(1)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Optional()
pageSize?: number;
}
export class TimelineStackResponseDto {

View File

@ -62,15 +62,21 @@ where
-- ActivityRepository.getStatistics
select
count(*) as "count"
count(*) filter (
where
"activity"."isLiked" = $1
) as "comments",
count(*) filter (
where
"activity"."isLiked" = $2
) as "likes"
from
"activity"
inner join "users" on "users"."id" = "activity"."userId"
and "users"."deletedAt" is null
left join "assets" on "assets"."id" = "activity"."assetId"
where
"activity"."assetId" = $1
and "activity"."albumId" = $2
and "activity"."isLiked" = $3
"activity"."assetId" = $3
and "activity"."albumId" = $4
and "assets"."deletedAt" is null
and "assets"."visibility" != 'locked'

View File

@ -67,19 +67,27 @@ export class ActivityRepository {
}
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] })
async getStatistics({ albumId, assetId }: { albumId: string; assetId?: string }): Promise<number> {
const { count } = await this.db
async getStatistics({
albumId,
assetId,
}: {
albumId: string;
assetId?: string;
}): Promise<{ comments: number; likes: number }> {
const result = await this.db
.selectFrom('activity')
.select((eb) => eb.fn.countAll<number>().as('count'))
.select((eb) => [
eb.fn.countAll<number>().filterWhere('activity.isLiked', '=', false).as('comments'),
eb.fn.countAll<number>().filterWhere('activity.isLiked', '=', true).as('likes'),
])
.innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null))
.leftJoin('assets', 'assets.id', 'activity.assetId')
.$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!))
.where('activity.albumId', '=', albumId)
.where('activity.isLiked', '=', false)
.where('assets.deletedAt', 'is', null)
.where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED))
.executeTakeFirstOrThrow();
return count;
return result;
}
}

View File

@ -54,13 +54,13 @@ describe(ActivityService.name, () => {
});
describe('getStatistics', () => {
it('should get the comment count', async () => {
it('should get the comment and like count', async () => {
const [albumId, assetId] = newUuids();
mocks.activity.getStatistics.mockResolvedValue(1);
mocks.activity.getStatistics.mockResolvedValue({ comments: 1, likes: 3 });
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1 });
await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1, likes: 3 });
});
});

View File

@ -31,7 +31,7 @@ export class ActivityService extends BaseService {
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
return { comments: await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }) };
return await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId });
}
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {

344
web/package-lock.json generated
View File

@ -2229,9 +2229,9 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz",
"integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz",
"integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2241,13 +2241,13 @@
"lightningcss": "1.30.1",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.8"
"tailwindcss": "4.1.7"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz",
"integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz",
"integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -2259,24 +2259,24 @@
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.8",
"@tailwindcss/oxide-darwin-arm64": "4.1.8",
"@tailwindcss/oxide-darwin-x64": "4.1.8",
"@tailwindcss/oxide-freebsd-x64": "4.1.8",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.8",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.8",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.8",
"@tailwindcss/oxide-linux-x64-musl": "4.1.8",
"@tailwindcss/oxide-wasm32-wasi": "4.1.8",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.8",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.8"
"@tailwindcss/oxide-android-arm64": "4.1.7",
"@tailwindcss/oxide-darwin-arm64": "4.1.7",
"@tailwindcss/oxide-darwin-x64": "4.1.7",
"@tailwindcss/oxide-freebsd-x64": "4.1.7",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.7",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.7",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.7",
"@tailwindcss/oxide-linux-x64-musl": "4.1.7",
"@tailwindcss/oxide-wasm32-wasi": "4.1.7",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.7",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.7"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz",
"integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz",
"integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==",
"cpu": [
"arm64"
],
@ -2291,9 +2291,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz",
"integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz",
"integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==",
"cpu": [
"arm64"
],
@ -2308,9 +2308,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz",
"integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz",
"integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==",
"cpu": [
"x64"
],
@ -2325,9 +2325,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz",
"integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz",
"integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==",
"cpu": [
"x64"
],
@ -2342,9 +2342,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz",
"integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz",
"integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==",
"cpu": [
"arm"
],
@ -2359,9 +2359,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz",
"integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz",
"integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==",
"cpu": [
"arm64"
],
@ -2376,9 +2376,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz",
"integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz",
"integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==",
"cpu": [
"arm64"
],
@ -2393,9 +2393,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz",
"integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz",
"integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==",
"cpu": [
"x64"
],
@ -2410,9 +2410,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz",
"integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz",
"integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==",
"cpu": [
"x64"
],
@ -2427,9 +2427,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz",
"integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz",
"integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@ -2448,7 +2448,7 @@
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.10",
"@napi-rs/wasm-runtime": "^0.2.9",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
@ -2456,10 +2456,70 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.3",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.2",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.3",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.2",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.9",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.0",
"@emnapi/runtime": "^1.4.0",
"@tybys/wasm-util": "^0.9.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.9.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.0",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz",
"integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz",
"integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==",
"cpu": [
"arm64"
],
@ -2474,9 +2534,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz",
"integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz",
"integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==",
"cpu": [
"x64"
],
@ -2568,15 +2628,15 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.8.tgz",
"integrity": "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz",
"integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.8",
"@tailwindcss/oxide": "4.1.8",
"tailwindcss": "4.1.8"
"@tailwindcss/node": "4.1.7",
"@tailwindcss/oxide": "4.1.7",
"tailwindcss": "4.1.7"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
@ -2888,17 +2948,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -2912,7 +2972,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -2928,16 +2988,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4"
},
"engines": {
@ -2952,34 +3012,15 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2989,32 +3030,15 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -3031,9 +3055,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"dev": true,
"license": "MIT",
"engines": {
@ -3045,16 +3069,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -3100,16 +3122,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3124,13 +3146,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/types": "8.32.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -4957,9 +4979,9 @@
}
},
"node_modules/fabric": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.6.tgz",
"integrity": "sha512-cL0m/RanEIiP67/TAj8kAQcEYlXofeB1SXCB1w7a0ktyUQHdRpnm2/VHlqsD/PfSLlGqftHzmxAS4LvKzSlrEw==",
"version": "6.6.5",
"resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.5.tgz",
"integrity": "sha512-BFxyLDeLMMgtteqQwKAyRM+oSkf82lDFzsiC7AMob7k7ag7naFuHOtWtcll4v+M9Cpn5aqRBfz1shnsO0vZhbg==",
"license": "MIT",
"engines": {
"node": ">=16.20.0"
@ -5429,9 +5451,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true,
"license": "MIT",
"engines": {
@ -8612,9 +8634,9 @@
}
},
"node_modules/svelte": {
"version": "5.33.6",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.6.tgz",
"integrity": "sha512-bxg2QY03JlrilCZmDlshY95Argj0rnX43UQFWZN4fct8PZTNBBmvfow2A6yOW1+YweDjhC2qdZF66ASI0Y21Tw==",
"version": "5.33.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.1.tgz",
"integrity": "sha512-7znzaaQALL62NBzkdKV04tmYIVla8qjrW+k6GdgFZcKcj8XOb8iEjmfRPo40iaWZlKv3+uiuc0h4iaGgwoORtA==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
@ -8863,9 +8885,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
"integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
"integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==",
"license": "MIT"
},
"node_modules/tapable": {
@ -9189,15 +9211,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -7,25 +7,29 @@
interface Props {
isLiked: ActivityResponseDto | null;
numberOfComments: number | undefined;
numberOfLikes: number | undefined;
disabled: boolean;
onOpenActivityTab: () => void;
onFavorite: () => void;
}
let { isLiked, numberOfComments, disabled, onOpenActivityTab, onFavorite }: Props = $props();
let { isLiked, numberOfComments, numberOfLikes, disabled, onOpenActivityTab, onFavorite }: Props = $props();
</script>
<div class="w-full flex p-4 text-white items-center justify-center rounded-full gap-5 bg-immich-dark-bg bg-opacity-60">
<div class="w-full flex p-4 items-center justify-center rounded-full gap-5 bg-subtle border bg-opacity-60">
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} onclick={onFavorite} {disabled}>
<div class="items-center justify-center">
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
<div class="flex gap-2 items-center justify-center">
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} class={isLiked ? 'text-red-400' : 'text-fg'} />
{#if numberOfLikes}
<div class="text-l">{numberOfLikes.toLocaleString($locale)}</div>
{/if}
</div>
</button>
<button type="button" onclick={onOpenActivityTab}>
<div class="flex gap-2 items-center justify-center">
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
{#if numberOfComments}
<div class="text-xl">{numberOfComments.toLocaleString($locale)}</div>
<div class="text-l">{numberOfComments.toLocaleString($locale)}</div>
{/if}
</div>
</button>

View File

@ -118,12 +118,9 @@
};
</script>
<div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}>
<div class="dark:bg-immich-dark-bg dark:text-immich-dark-fg w-full h-full">
<div
class="flex w-full h-fit dark:bg-immich-dark-bg dark:text-immich-dark-fg p-2 bg-white"
bind:clientHeight={activityHeight}
>
<div class="overflow-y-hidden relative h-full border-l border-subtle bg-subtle" bind:offsetHeight={innerHeight}>
<div class="w-full h-full">
<div class="flex w-full h-fit dark:text-immich-dark-fg p-2 bg-subtle" bind:clientHeight={activityHeight}>
<div class="flex place-items-center gap-2">
<IconButton
shape="round"

View File

@ -513,6 +513,7 @@
disabled={!album?.isActivityEnabled}
isLiked={activityManager.isLiked}
numberOfComments={activityManager.commentCount}
numberOfLikes={activityManager.likeCount}
onFavorite={handleFavorite}
onOpenActivityTab={handleOpenActivity}
/>
@ -569,6 +570,7 @@
asset={toTimelineAsset(stackedAsset)}
onClick={() => {
asset = stackedAsset;
previewStackedAsset = undefined;
}}
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
readonly

View File

@ -70,7 +70,9 @@
title="Add tag"
onclick={handleAddTag}
>
<span class="text-sm px-1 flex place-items-center place-content-center gap-1"><Icon path={mdiPlus} />Add</span>
<span class="text-sm px-1 flex place-items-center place-content-center gap-1"
><Icon path={mdiPlus} />{$t('add')}</span
>
</button>
</section>
</section>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { featureFlags } from '$lib/stores/server-config.store';
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js';
import { t } from 'svelte-i18n';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
@ -10,11 +10,12 @@
interface Props {
onAssetDelete: OnDelete;
onUndoDelete?: OnUndoDelete | undefined;
menuItem?: boolean;
force?: boolean;
}
let { onAssetDelete, menuItem = false, force = !$featureFlags.trash }: Props = $props();
let { onAssetDelete, onUndoDelete = undefined, menuItem = false, force = !$featureFlags.trash }: Props = $props();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
@ -34,8 +35,8 @@
const handleDelete = async () => {
loading = true;
const ids = [...getOwnedAssets()].map((a) => a.id);
await deleteAssets(force, onAssetDelete, ids);
const assets = [...getOwnedAssets()];
await deleteAssets(force, onAssetDelete, assets, onUndoDelete);
clearSelect();
isShowConfirmation = false;
loading = false;

View File

@ -382,7 +382,12 @@
const trashOrDelete = async (force: boolean = false) => {
isShowDeleteConfirmation = false;
await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets);
await deleteAssets(
!(isTrashEnabled && !force),
(assetIds) => assetStore.removeAssets(assetIds),
assetInteraction.selectedAssets,
!isTrashEnabled || force ? undefined : (assets) => assetStore.addAssets(assets),
);
assetInteraction.clearMultiselect();
};

View File

@ -38,6 +38,7 @@
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
onReload?: (() => void) | undefined;
pageHeaderOffset?: number;
slidingWindowOffset?: number;
}
@ -54,6 +55,7 @@
onPrevious = undefined,
onNext = undefined,
onRandom = undefined,
onReload = undefined,
slidingWindowOffset = 0,
pageHeaderOffset = 0,
}: Props = $props();
@ -255,7 +257,8 @@
await deleteAssets(
!(isTrashEnabled && !force),
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
idsSelectedAssets,
assetInteraction.selectedAssets,
onReload,
);
assetInteraction.clearMultiselect();
};
@ -426,7 +429,6 @@
};
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map((selectedAsset) => selectedAsset.id));
$effect(() => {
if (!lastAssetMouseEvent) {

View File

@ -1,13 +1,20 @@
<script lang="ts">
import { Theme } from '$lib/constants';
import { defaultLang, langs, Theme } from '$lib/constants';
import { themeManager } from '$lib/managers/theme-manager.svelte';
import { lang } from '$lib/stores/preferences.store';
import { ThemeSwitcher } from '@immich/ui';
import { get } from 'svelte/store';
</script>
{#if !themeManager.theme.system}
{#await langs
.find((item) => item.code === get(lang))
?.loader() ?? defaultLang.loader() then { default: translations }}
<ThemeSwitcher
size="medium"
color="secondary"
{translations}
onChange={(theme) => themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)}
/>
{/await}
{/if}

View File

@ -39,7 +39,7 @@
};
const handleToggleLocaleBrowser = () => {
$locale = $locale ? undefined : fallbackLocale.code;
$locale = $locale === 'default' ? fallbackLocale.code : 'default';
};
const handleLocaleChange = (newLocale: string | undefined) => {
@ -89,13 +89,13 @@
<SettingSwitch
title={$t('default_locale')}
subtitle={$t('default_locale_description')}
checked={$locale == undefined}
checked={$locale == 'default'}
onToggle={handleToggleLocaleBrowser}
>
<p class="mt-2 dark:text-gray-400">{selectedDate}</p>
</SettingSwitch>
</div>
{#if $locale !== undefined}
{#if $locale !== 'default'}
<div class="ms-4">
<SettingCombobox
comboboxPlaceholder={$t('searching_locales')}
@ -113,7 +113,6 @@
title={$t('display_original_photos')}
subtitle={$t('display_original_photos_setting_description')}
bind:checked={$alwaysLoadOriginalFile}
onToggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
/>
</div>
<div class="ms-4">
@ -121,16 +120,10 @@
title={$t('video_hover_setting')}
subtitle={$t('video_hover_setting_description')}
bind:checked={$playVideoThumbnailOnHover}
onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
/>
</div>
<div class="ms-4">
<SettingSwitch
title={$t('loop_videos')}
subtitle={$t('loop_videos_description')}
bind:checked={$loopVideo}
onToggle={() => ($loopVideo = !$loopVideo)}
/>
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
</div>
<div class="ms-4">

View File

@ -273,9 +273,17 @@ export const locales = [
{ code: 'zu-ZA', name: 'Zulu (South Africa)' },
];
export const defaultLang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') };
interface Lang {
name: string;
code: string;
loader: () => Promise<{ default: object }>;
rtl?: boolean;
weblateCode?: string;
}
export const langs = [
export const defaultLang: Lang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') };
export const langs: Lang[] = [
{ name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') },
{ name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json'), rtl: true },
{ name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json'), rtl: true },
@ -359,7 +367,7 @@ export const langs = [
weblateCode: 'zh_SIMPLIFIED',
loader: () => import('$i18n/zh_SIMPLIFIED.json'),
},
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) },
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({ default: {} }) },
];
export enum ImmichProduct {

View File

@ -17,6 +17,7 @@ class ActivityManager {
#assetId = $state<string | undefined>();
#activities = $state<ActivityResponseDto[]>([]);
#commentCount = $state(0);
#likeCount = $state(0);
#isLiked = $state<ActivityResponseDto | null>(null);
get activities() {
@ -27,6 +28,10 @@ class ActivityManager {
return this.#commentCount;
}
get likeCount() {
return this.#likeCount;
}
get isLiked() {
return this.#isLiked;
}
@ -48,6 +53,10 @@ class ActivityManager {
this.#commentCount++;
}
if (activity.type === ReactionType.Like) {
this.#likeCount++;
}
handlePromiseError(this.refreshActivities(this.#albumId, this.#assetId));
return activity;
}
@ -61,6 +70,10 @@ class ActivityManager {
this.#commentCount--;
}
if (activity.type === ReactionType.Like) {
this.#likeCount--;
}
this.#activities = index
? this.#activities.splice(index, 1)
: this.#activities.filter(({ id }) => id !== activity.id);
@ -98,8 +111,9 @@ class ActivityManager {
});
this.#isLiked = liked ?? null;
const { comments } = await getActivityStatistics({ albumId, assetId });
const { comments, likes } = await getActivityStatistics({ albumId, assetId });
this.#commentCount = comments;
this.#likeCount = likes;
}
reset() {
@ -107,6 +121,7 @@ class ActivityManager {
this.#assetId = undefined;
this.#activities = [];
this.#commentCount = 0;
this.#likeCount = 0;
}
}

View File

@ -9,9 +9,9 @@ export interface ThemeSetting {
}
// Locale to use for formatting dates, numbers, etc.
export const locale = persisted<string | undefined>('locale', undefined, {
export const locale = persisted<string | undefined>('locale', 'default', {
serializer: {
parse: (text) => (text == '' ? 'en-US' : text),
parse: (text) => text || 'default',
stringify: (object) => object ?? '',
},
});

View File

@ -1,12 +1,13 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
import type { StackResponse } from '$lib/utils/asset-utils';
import { AssetVisibility, deleteAssets as deleteBulk } from '@immich/sdk';
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
export type OnDelete = (assetIds: string[]) => void;
export type OnUndoDelete = (assets: TimelineAsset[]) => void;
export type OnRestore = (ids: string[]) => void;
export type OnLink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
@ -17,9 +18,15 @@ export type OnStack = (result: StackResponse) => void;
export type OnUnstack = (assets: TimelineAsset[]) => void;
export type OnSetVisibility = (ids: string[]) => void;
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
export const deleteAssets = async (
force: boolean,
onAssetDelete: OnDelete,
assets: TimelineAsset[],
onUndoDelete: OnUndoDelete | undefined = undefined,
) => {
const $t = get(t);
try {
const ids = assets.map((a) => a.id);
await deleteBulk({ assetBulkDeleteDto: { ids, force } });
onAssetDelete(ids);
@ -28,12 +35,28 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids:
? $t('assets_permanently_deleted_count', { values: { count: ids.length } })
: $t('assets_trashed_count', { values: { count: ids.length } }),
type: NotificationType.Info,
...(onUndoDelete &&
!force && {
button: { text: $t('undo'), onClick: () => undoDeleteAssets(onUndoDelete, assets) },
timeout: 5000,
}),
});
} catch (error) {
handleError(error, $t('errors.unable_to_delete_assets'));
}
};
const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsset[]) => {
const $t = get(t);
try {
const ids = assets.map((a) => a.id);
await restoreAssets({ bulkIdsDto: { ids } });
onUndoDelete?.(assets);
} catch (error) {
handleError(error, $t('errors.unable_to_restore_assets'));
}
};
/**
* Update the asset stack state in the asset store based on the provided stack response.
* This function updates the stack information so that the icon is shown for the primary asset

View File

@ -1,3 +1,4 @@
import { locale } from '$lib/stores/preferences.store';
import { parseUtcDate } from '$lib/utils/date-time';
import { formatGroupTitle } from '$lib/utils/timeline-util';
import { DateTime } from 'luxon';
@ -16,48 +17,63 @@ describe('formatGroupTitle', () => {
it('formats today', () => {
const date = parseUtcDate('2024-07-27T01:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('today');
expect(formatGroupTitle(date.setLocale('es'))).toBe('hoy');
locale.set('en');
expect(formatGroupTitle(date)).toBe('today');
locale.set('es');
expect(formatGroupTitle(date)).toBe('hoy');
});
it('formats yesterday', () => {
const date = parseUtcDate('2024-07-26T23:59:59Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('yesterday');
expect(formatGroupTitle(date.setLocale('fr'))).toBe('hier');
locale.set('en');
expect(formatGroupTitle(date)).toBe('yesterday');
locale.set('fr');
expect(formatGroupTitle(date)).toBe('hier');
});
it('formats last week', () => {
const date = parseUtcDate('2024-07-21T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Sunday');
expect(formatGroupTitle(date.setLocale('ar-SA'))).toBe('الأحد');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Sunday');
locale.set('ar-SA');
expect(formatGroupTitle(date)).toBe('الأحد');
});
it('formats date 7 days ago', () => {
const date = parseUtcDate('2024-07-20T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Sat, Jul 20');
expect(formatGroupTitle(date.setLocale('de'))).toBe('Sa., 20. Juli');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Sat, Jul 20');
locale.set('de');
expect(formatGroupTitle(date)).toBe('Sa., 20. Juli');
});
it('formats date this year', () => {
const date = parseUtcDate('2020-01-01T00:00:00Z');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Wed, Jan 1, 2020');
expect(formatGroupTitle(date.setLocale('ja'))).toBe('2020年1月1日(水)');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Wed, Jan 1, 2020');
locale.set('ja');
expect(formatGroupTitle(date)).toBe('2020年1月1日(水)');
});
it('formats future date', () => {
const tomorrow = parseUtcDate('2024-07-28T00:00:00Z');
expect(formatGroupTitle(tomorrow.setLocale('en'))).toBe('Sun, Jul 28');
locale.set('en');
expect(formatGroupTitle(tomorrow)).toBe('Sun, Jul 28');
const nextMonth = parseUtcDate('2024-08-28T00:00:00Z');
expect(formatGroupTitle(nextMonth.setLocale('en'))).toBe('Wed, Aug 28');
locale.set('en');
expect(formatGroupTitle(nextMonth)).toBe('Wed, Aug 28');
const nextYear = parseUtcDate('2025-01-10T12:00:00Z');
expect(formatGroupTitle(nextYear.setLocale('en'))).toBe('Fri, Jan 10, 2025');
locale.set('en');
expect(formatGroupTitle(nextYear)).toBe('Fri, Jan 10, 2025');
});
it('returns "Invalid DateTime" when date is invalid', () => {
const date = DateTime.invalid('test');
expect(formatGroupTitle(date.setLocale('en'))).toBe('Invalid DateTime');
expect(formatGroupTitle(date.setLocale('es'))).toBe('Invalid DateTime');
locale.set('en');
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
locale.set('es');
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
});
});

View File

@ -62,12 +62,12 @@ export function formatGroupTitle(_date: DateTime): string {
// Today
if (today.hasSame(date, 'day')) {
return date.toRelativeCalendar();
return date.toRelativeCalendar({ locale: get(locale) });
}
// Yesterday
if (today.minus({ days: 1 }).hasSame(date, 'day')) {
return date.toRelativeCalendar();
return date.toRelativeCalendar({ locale: get(locale) });
}
// Last week

View File

@ -87,6 +87,7 @@
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import { type TimelineAsset } from '$lib/stores/assets-store.svelte';
interface Props {
data: PageData;
@ -317,6 +318,11 @@
await refreshAlbum();
};
const handleUndoRemoveAssets = async (assets: TimelineAsset[]) => {
assetStore.addAssets(assets);
await refreshAlbum();
};
const handleUpdateThumbnail = async (assetId: string) => {
if (viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL) {
return;
@ -570,6 +576,7 @@
disabled={!album.isActivityEnabled}
isLiked={activityManager.isLiked}
numberOfComments={activityManager.commentCount}
numberOfLikes={undefined}
onFavorite={handleFavorite}
onOpenActivityTab={handleOpenAndCloseActivityTab}
/>
@ -623,7 +630,7 @@
<RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
{/if}
{#if assetInteraction.isAllUserOwned}
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} />
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} onUndoDelete={handleUndoRemoveAssets} />
{/if}
</ButtonContextMenu>
</AssetSelectControlBar>

View File

@ -13,6 +13,7 @@
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { AssetVisibility } from '@immich/sdk';
@ -20,7 +21,6 @@
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
interface Props {
data: PageData;

View File

@ -92,7 +92,11 @@
<TagAction menuItem />
{/if}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<DeleteAssets
menuItem
onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)}
onUndoDelete={(assets) => assetStore.addAssets(assets)}
/>
</ButtonContextMenu>
</AssetSelectControlBar>
{/if}

View File

@ -122,6 +122,7 @@
{viewport}
showAssetName={true}
pageHeaderOffset={54}
onReload={triggerAssetUpdate}
/>
</div>
{/if}
@ -170,7 +171,7 @@
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem />
{/if}
<DeleteAssets menuItem onAssetDelete={triggerAssetUpdate} />
<DeleteAssets menuItem onAssetDelete={triggerAssetUpdate} onUndoDelete={triggerAssetUpdate} />
<hr />
<AssetJobActions />
</ButtonContextMenu>

View File

@ -351,6 +351,11 @@
await updateAssetCount();
};
const handleUndoDeleteAssets = async (assets: TimelineAsset[]) => {
assetStore.addAssets(assets);
await updateAssetCount();
};
let person = $derived(data.person);
let thumbnailData = $derived(getPeopleThumbnailUrl(person));
@ -532,7 +537,11 @@
<TagAction menuItem />
{/if}
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<DeleteAssets menuItem onAssetDelete={(assetIds) => handleDeleteAssets(assetIds)} />
<DeleteAssets
menuItem
onAssetDelete={(assetIds) => handleDeleteAssets(assetIds)}
onUndoDelete={(assets) => handleUndoDeleteAssets(assets)}
/>
</ButtonContextMenu>
</AssetSelectControlBar>
{:else}

View File

@ -150,7 +150,11 @@
{#if $preferences.tags.enabled}
<TagAction menuItem />
{/if}
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<DeleteAssets
menuItem
onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)}
onUndoDelete={(assets) => assetStore.addAssets(assets)}
/>
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
<hr />
<AssetJobActions />

View File

@ -301,7 +301,7 @@
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem />
{/if}
<DeleteAssets menuItem {onAssetDelete} />
<DeleteAssets menuItem {onAssetDelete} onUndoDelete={onSearchQueryUpdate} />
<hr />
<AssetJobActions />
</ButtonContextMenu>
@ -382,6 +382,7 @@
showArchiveIcon={true}
{viewport}
pageHeaderOffset={54}
onReload={onSearchQueryUpdate}
/>
{:else if !isLoading}
<div class="flex min-h-[calc(66vh-11rem)] w-full place-content-center items-center dark:text-white">
@ -444,7 +445,7 @@
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem />
{/if}
<DeleteAssets menuItem {onAssetDelete} />
<DeleteAssets menuItem {onAssetDelete} onUndoDelete={onSearchQueryUpdate} />
<hr />
<AssetJobActions />
</ButtonContextMenu>