Compare commits

..

13 Commits

Author SHA1 Message Date
Alex The Bot
126dd45751 Version v1.81.1 2023-10-04 17:53:42 +00:00
Daniel Dietzler
ff331ffad9 fix(server): Offset of random endpoint could be higher than user's asset count (#4342)
* fix offset of all assets with correct ownerId

* (e2e): test if user does not have all assets
2023-10-04 12:51:44 -05:00
Daniel Dietzler
e571880c16 feat(web, mobile): Options to show archived assets in map (#4293)
* Add include archive setting to map on web

* open api

* better naming for web isArchived variable

* add withArchived setting to mobile

* (e2e): tests for mapMarker endpoint and isArchived

* isArchived to mobile

* chore: cleanup test

* chore: optimize e2e

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-10-04 09:51:07 -04:00
markeeisner
e5b4d09827 Remove /etc/timezone volume mount from compose (#4336) 2023-10-04 04:01:00 -05:00
Alex The Bot
81d51fbd7e Version v1.81.0 2023-10-03 20:48:23 +00:00
Alex
02f9b40d67 fix(server): library control doesn't apply to new library from the third row (#4331) 2023-10-03 14:05:14 -05:00
Jason Rasmussen
260a600bbc chore(server): dev compose changes (#4316) 2023-10-03 13:06:08 -05:00
Jason Rasmussen
818005fcb5 fix(server): fallback to local timezone when rendering storage template (#4317) 2023-10-03 13:05:44 -05:00
Daniel Dietzler
e5f704cf3b fix asset upload permissions for shared links (#4325) 2023-10-03 12:36:51 -04:00
Jonathan Jogenfors
e2f1e38472 chore(server,web): bump node version to 20.8 (#4311)
* chore: bump node version to 20.8

* fix: remove node hash
2023-10-03 09:34:35 -05:00
Alex
b3c82d5ba2 fix(server): incorrect video creation date EXIF extraction (#4309)
* fix(server): incorrect video creation date EXIF extraction

* update dependency

* update dependency

* revert

* remove unused code
2023-10-03 08:51:40 -05:00
Jonathan Jogenfors
6d1868a6e0 feat: server containers use host timezone (#4313) 2023-10-02 20:50:27 -05:00
Daniel Dietzler
98db9331d8 fix(server): delete face thumbnails when merging people (#4310)
* new job for person deletion, including face thumbnail deletion

* fix tests, delete files directly instead queueing jobs
2023-10-02 21:15:11 -04:00
52 changed files with 454 additions and 232 deletions

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -6278,13 +6278,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMapMarkers: async (isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getMapMarkers: async (isArchived?: boolean, isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/map-marker`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -6306,6 +6307,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (isArchived !== undefined) {
localVarQueryParameter['isArchived'] = isArchived;
}
if (isFavorite !== undefined) {
localVarQueryParameter['isFavorite'] = isFavorite;
}
@@ -7134,14 +7139,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options);
async getMapMarkers(isArchived?: boolean, isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -7428,7 +7434,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getMapMarkers(requestParameters: AssetApiGetMapMarkersRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<MapMarkerResponseDto>> {
return localVarFp.getMapMarkers(requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(axios, basePath));
return localVarFp.getMapMarkers(requestParameters.isArchived, requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(axios, basePath));
},
/**
*
@@ -7846,6 +7852,13 @@ export interface AssetApiGetDownloadInfoRequest {
* @interface AssetApiGetMapMarkersRequest
*/
export interface AssetApiGetMapMarkersRequest {
/**
*
* @type {boolean}
* @memberof AssetApiGetMapMarkers
*/
readonly isArchived?: boolean
/**
*
* @type {boolean}
@@ -8374,7 +8387,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getMapMarkers(requestParameters: AssetApiGetMapMarkersRequest = {}, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getMapMarkers(requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getMapMarkers(requestParameters.isArchived, requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(this.axios, this.basePath));
}
/**

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -11,8 +11,9 @@ services:
command: npm run start:debug immich
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
ports:
- 3001:3001
- 9230:9230
@@ -25,25 +26,6 @@ services:
- database
- typesense
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning-dev:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: unless-stopped
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
@@ -57,8 +39,9 @@ services:
command: npm run start:debug microservices
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
@@ -94,6 +77,25 @@ services:
depends_on:
- immich-server
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning-dev:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
ports:
- 3003:3003
volumes:
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
restart: unless-stopped
typesense:
container_name: immich_typesense
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
@@ -103,7 +105,7 @@ services:
# remove this to get debug messages
- GLOG_minloglevel=1
volumes:
- tsdata:/data
- ${UPLOAD_LOCATION}/typesense:/data
redis:
container_name: immich_redis
@@ -119,7 +121,7 @@ services:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
- ${UPLOAD_LOCATION}/postgres:/data
ports:
- 5432:5432
@@ -141,6 +143,4 @@ services:
restart: unless-stopped
volumes:
pgdata:
model-cache:
tsdata:

View File

@@ -10,6 +10,7 @@ services:
command: ["./start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
@@ -29,7 +30,7 @@ services:
env_file:
- .env
restart: always
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
@@ -42,6 +43,7 @@ services:
command: ["./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:

View File

@@ -4,9 +4,10 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
command: ["start.sh", "immich"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
@@ -21,9 +22,10 @@ services:
# extends:
# file: hwaccel.yml
# service: hwaccel
command: [ "start.sh", "microservices" ]
command: ["start.sh", "microservices"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.80.0"
version = "1.81.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 104,
"android.injected.version.name" => "1.80.0",
"android.injected.version.code" => 105,
"android.injected.version.name" => "1.81.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -312,6 +312,7 @@
"map_settings_dialog_title": "Map Settings",
"map_settings_dark_mode": "Dark mode",
"map_settings_only_show_favorites": "Show Favorite Only",
"map_settings_include_show_archived": "Include Archived",
"map_settings_only_relative_range": "Date range",
"map_settings_dialog_cancel": "Cancel",
"map_settings_dialog_save": "Save",

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.80.0"
version_number: "1.81.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -1,29 +1,33 @@
class MapState {
final bool isDarkTheme;
final bool showFavoriteOnly;
final bool includeArchived;
final int relativeTime;
MapState({
this.isDarkTheme = false,
this.showFavoriteOnly = false,
this.includeArchived = false,
this.relativeTime = 0,
});
MapState copyWith({
bool? isDarkTheme,
bool? showFavoriteOnly,
bool? includeArchived,
int? relativeTime,
}) {
return MapState(
isDarkTheme: isDarkTheme ?? this.isDarkTheme,
showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly,
includeArchived: includeArchived ?? this.includeArchived,
relativeTime: relativeTime ?? this.relativeTime,
);
}
@override
String toString() {
return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime)';
return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived)';
}
@override
@@ -33,13 +37,15 @@ class MapState {
return other is MapState &&
other.isDarkTheme == isDarkTheme &&
other.showFavoriteOnly == showFavoriteOnly &&
other.relativeTime == relativeTime;
other.relativeTime == relativeTime &&
other.includeArchived == includeArchived;
}
@override
int get hashCode {
return isDarkTheme.hashCode ^
showFavoriteOnly.hashCode ^
relativeTime.hashCode;
relativeTime.hashCode ^
includeArchived.hashCode;
}
}

View File

@@ -10,6 +10,7 @@ final mapMarkersProvider =
final mapState = ref.read(mapStateNotifier);
DateTime? fileCreatedAfter;
bool? isFavorite;
bool? isIncludeArchived;
if (mapState.relativeTime != 0) {
fileCreatedAfter =
@@ -20,8 +21,13 @@ final mapMarkersProvider =
isFavorite = true;
}
if (!mapState.includeArchived) {
isIncludeArchived = false;
}
final markers = await service.getMapMarkers(
isFavorite: isFavorite,
withArchived: isIncludeArchived,
fileCreatedAfter: fileCreatedAfter,
);

View File

@@ -11,6 +11,8 @@ class MapStateNotifier extends StateNotifier<MapState> {
.getSetting<bool>(AppSettingsEnum.mapThemeMode),
showFavoriteOnly: appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: appSettingsProvider
.getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
relativeTime: appSettingsProvider
.getSetting<int>(AppSettingsEnum.mapRelativeDate),
),
@@ -31,11 +33,19 @@ class MapStateNotifier extends StateNotifier<MapState> {
void switchFavoriteOnly(bool isFavoriteOnly) {
appSettingsProvider.setSetting(
AppSettingsEnum.mapShowFavoriteOnly,
appSettingsProvider,
isFavoriteOnly,
);
state = state.copyWith(showFavoriteOnly: isFavoriteOnly);
}
void switchIncludeArchived(bool isIncludeArchived) {
appSettingsProvider.setSetting(
AppSettingsEnum.mapIncludeArchived,
isIncludeArchived,
);
state = state.copyWith(includeArchived: isIncludeArchived);
}
void setRelativeTime(int relativeTime) {
appSettingsProvider.setSetting(
AppSettingsEnum.mapRelativeDate,

View File

@@ -23,12 +23,14 @@ class MapSerivce {
Future<List<MapMarkerResponseDto>> getMapMarkers({
bool? isFavorite,
bool? withArchived,
DateTime? fileCreatedAfter,
DateTime? fileCreatedBefore,
}) async {
try {
final markers = await _apiService.assetApi.getMapMarkers(
isFavorite: isFavorite,
isArchived: withArchived,
fileCreatedAfter: fileCreatedAfter,
fileCreatedBefore: fileCreatedBefore,
);

View File

@@ -13,6 +13,7 @@ class MapSettingsDialog extends HookConsumerWidget {
final mapSettings = ref.read(mapStateNotifier);
final isDarkMode = useState(mapSettings.isDarkTheme);
final showFavoriteOnly = useState(mapSettings.showFavoriteOnly);
final showIncludeArchived = useState(mapSettings.includeArchived);
final showRelativeDate = useState(mapSettings.relativeTime);
final ThemeData theme = Theme.of(context);
@@ -48,6 +49,22 @@ class MapSettingsDialog extends HookConsumerWidget {
);
}
Widget buildIncludeArchivedSetting() {
return SwitchListTile.adaptive(
value: showIncludeArchived.value,
onChanged: (value) {
showIncludeArchived.value = value;
},
activeColor: theme.primaryColor,
dense: true,
title: Text(
"map_settings_include_show_archived".tr(),
style:
theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
),
);
}
Widget buildDateRangeSetting() {
final now = DateTime.now();
return DropdownMenu(
@@ -127,6 +144,8 @@ class MapSettingsDialog extends HookConsumerWidget {
mapSettingsNotifier.switchTheme(isDarkMode.value);
mapSettingsNotifier.switchFavoriteOnly(showFavoriteOnly.value);
mapSettingsNotifier.setRelativeTime(showRelativeDate.value);
mapSettingsNotifier
.switchIncludeArchived(showIncludeArchived.value);
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
@@ -166,6 +185,7 @@ class MapSettingsDialog extends HookConsumerWidget {
children: [
buildMapThemeSetting(),
buildFavoriteOnlySetting(),
buildIncludeArchivedSetting(),
const SizedBox(
height: 10,
),

View File

@@ -155,7 +155,8 @@ class MapPageState extends ConsumerState<MapPage> {
ref.listen(mapStateNotifier, (previous, next) {
bool shouldRefetch =
previous?.showFavoriteOnly != next.showFavoriteOnly ||
previous?.relativeTime != next.relativeTime;
previous?.relativeTime != next.relativeTime ||
previous?.includeArchived != next.includeArchived;
if (shouldRefetch) {
refetchMarkers.value = shouldRefetch;
ref.invalidate(mapMarkersProvider);

View File

@@ -48,6 +48,7 @@ enum AppSettingsEnum<T> {
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
mapThemeMode<bool>(StoreKey.mapThemeMode, null, false),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
;

View File

@@ -179,6 +179,7 @@ enum StoreKey<T> {
mapShowFavoriteOnly<bool>(118, type: bool),
mapRelativeDate<int>(119, type: int),
selfSignedCert<bool>(120, type: bool),
mapIncludeArchived<bool>(121, type: bool),
;
const StoreKey(

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.80.0
- API version: 1.81.1
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements

View File

@@ -902,7 +902,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getMapMarkers**
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
> List<MapMarkerResponseDto> getMapMarkers(isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore)
@@ -925,12 +925,13 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final isArchived = true; // bool |
final isFavorite = true; // bool |
final fileCreatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final fileCreatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
try {
final result = api_instance.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore);
final result = api_instance.getMapMarkers(isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMapMarkers: $e\n');
@@ -941,6 +942,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isArchived** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**fileCreatedAfter** | **DateTime**| | [optional]
**fileCreatedBefore** | **DateTime**| | [optional]

View File

@@ -909,12 +909,14 @@ class AssetApi {
/// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response].
/// Parameters:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
Future<Response> getMapMarkersWithHttpInfo({ bool? isArchived, bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/map-marker';
@@ -925,6 +927,9 @@ class AssetApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@@ -951,13 +956,15 @@ class AssetApi {
/// Parameters:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, );
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isArchived, bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
final response = await getMapMarkersWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -102,7 +102,7 @@ void main() {
// TODO
});
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isArchived, bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
test('test getMapMarkers', () async {
// TODO
});

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.80.0+104
version: 1.81.1+105
isar_version: &isar_version 3.1.0+1
environment:

View File

@@ -1,4 +1,4 @@
FROM node:18-bookworm@sha256:c85dc4392f44f5de1d0d72dd20a088a542734445f99bed7aa8ac895c706d370d as builder
FROM node:20.8-bookworm as builder
WORKDIR /usr/src/app
@@ -29,7 +29,7 @@ FROM builder as prod
RUN npm run build
RUN npm prune --omit=dev --omit=optional
FROM node:18-bookworm-slim@sha256:a0cca98f2896135d4c0386922211c1f90f98f27a58b8f2c07850d0fbe1c2104e
FROM node:20.8-bookworm
ENV NODE_ENV=production

View File

@@ -1406,6 +1406,14 @@
"get": {
"operationId": "getMapMarkers",
"parameters": [
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
@@ -5099,7 +5107,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.80.0",
"version": "1.81.1",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.80.0",
"version": "1.81.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.80.0",
"version": "1.81.1",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.80.0",
"version": "1.81.1",
"description": "",
"author": "",
"private": true,

View File

@@ -10,6 +10,7 @@ export enum Permission {
ASSET_SHARE = 'asset.share',
ASSET_VIEW = 'asset.view',
ASSET_DOWNLOAD = 'asset.download',
ASSET_UPLOAD = 'asset.upload',
// ALBUM_CREATE = 'album.create',
ALBUM_READ = 'album.read',
@@ -26,7 +27,6 @@ export enum Permission {
LIBRARY_CREATE = 'library.create',
LIBRARY_READ = 'library.read',
LIBRARY_WRITE = 'library.write',
LIBRARY_UPDATE = 'library.update',
LIBRARY_DELETE = 'library.delete',
LIBRARY_DOWNLOAD = 'library.download',
@@ -96,6 +96,9 @@ export class AccessCore {
case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id));
case Permission.ASSET_UPLOAD:
return authUser.isAllowUpload;
case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control
return this.repository.asset.hasOwnerAccess(authUser.id, id);
@@ -166,6 +169,9 @@ export class AccessCore {
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
);
case Permission.ASSET_UPLOAD:
return this.repository.library.hasOwnerAccess(authUser.id, id);
case Permission.ALBUM_REMOVE_ASSET:
return this.repository.album.hasOwnerAccess(authUser.id, id);
@@ -184,9 +190,6 @@ export class AccessCore {
(await this.repository.library.hasPartnerAccess(authUser.id, id))
);
case Permission.LIBRARY_WRITE:
return this.repository.library.hasOwnerAccess(authUser.id, id);
case Permission.LIBRARY_UPDATE:
return this.repository.library.hasOwnerAccess(authUser.id, id);

View File

@@ -22,6 +22,7 @@ export interface LivePhotoSearchOptions {
}
export interface MapMarkerSearchOptions {
isArchived?: boolean;
isFavorite?: boolean;
fileCreatedBefore?: Date;
fileCreatedAfter?: Date;

View File

@@ -4,6 +4,12 @@ import { IsBoolean, IsDate } from 'class-validator';
import { Optional, toBoolean } from '../../domain.util';
export class MapMarkerDto {
@ApiProperty()
@Optional()
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean;
@ApiProperty()
@Optional()
@IsBoolean()

View File

@@ -56,9 +56,10 @@ export enum JobName {
CLASSIFY_IMAGE = 'classify-image',
// facial recognition
PERSON_CLEANUP = 'person-cleanup',
PERSON_DELETE = 'person-delete',
QUEUE_RECOGNIZE_FACES = 'queue-recognize-faces',
RECOGNIZE_FACES = 'recognize-faces',
PERSON_CLEANUP = 'person-cleanup',
// library managment
LIBRARY_SCAN = 'library-refresh',
@@ -103,6 +104,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
[JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
[JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
[JobName.PERSON_DELETE]: QueueName.BACKGROUND_TASK,
// conversion
[JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,

View File

@@ -68,6 +68,7 @@ export type JobItem =
| { name: JobName.QUEUE_RECOGNIZE_FACES; data: IBaseJob }
| { name: JobName.RECOGNIZE_FACES; data: IEntityJob }
| { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob }
| { name: JobName.PERSON_DELETE; data: IEntityJob }
// Clip Embedding
| { name: JobName.QUEUE_ENCODE_CLIP; data: IBaseJob }

View File

@@ -311,7 +311,19 @@ export class MetadataService {
assetId: asset.id,
bitsPerSample: this.getBitsPerSample(tags),
colorspace: tags.ColorSpace ?? null,
dateTimeOriginal: exifDate(firstDateTime(tags as Tags)) ?? asset.fileCreatedAt,
dateTimeOriginal:
exifDate(
firstDateTime(tags as Tags, [
'SubSecDateTimeOriginal',
'DateTimeOriginal',
'SubSecCreateDate',
'CreationDate',
'CreateDate',
'SubSecMediaCreateDate',
'MediaCreateDate',
'DateTimeCreated',
]),
) ?? asset.fileCreatedAt,
exifImageHeight: validate(tags.ImageHeight),
exifImageWidth: validate(tags.ImageWidth),
exposureTime: tags.ExposureTime ?? null,

View File

@@ -373,11 +373,7 @@ describe(PersonService.name, () => {
await sut.handlePersonCleanup();
expect(personMock.delete).toHaveBeenCalledWith(personStub.noName);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES,
data: { files: ['/path/to/thumbnail.jpg'] },
});
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.PERSON_DELETE, data: { id: personStub.noName.id } });
});
});
@@ -409,7 +405,7 @@ describe(PersonService.name, () => {
items: [assetStub.image],
hasNextPage: false,
});
personMock.deleteAll.mockResolvedValue(5);
personMock.getAll.mockResolvedValue([personStub.withName]);
searchMock.deleteAllFaces.mockResolvedValue(100);
await sut.handleQueueRecognizeFaces({ force: true });
@@ -419,6 +415,10 @@ describe(PersonService.name, () => {
name: JobName.RECOGNIZE_FACES,
data: { id: assetStub.image.id },
});
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.PERSON_DELETE,
data: { id: personStub.withName.id },
});
});
});
@@ -650,7 +650,10 @@ describe(PersonService.name, () => {
oldPersonId: personStub.mergePerson.id,
});
expect(personMock.delete).toHaveBeenCalledWith(personStub.mergePerson);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.PERSON_DELETE,
data: { id: personStub.mergePerson.id },
});
expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
});

View File

@@ -139,16 +139,27 @@ export class PersonService {
return results;
}
async handlePersonDelete({ id }: IEntityJob) {
const person = await this.repository.getById(id);
if (!person) {
return false;
}
try {
await this.repository.delete(person);
await this.storageRepository.unlink(person.thumbnailPath);
} catch (error: Error | any) {
this.logger.error(`Unable to delete person: ${error}`, error?.stack);
}
return true;
}
async handlePersonCleanup() {
const people = await this.repository.getAllWithoutFaces();
for (const person of people) {
this.logger.debug(`Person ${person.name || person.id} no longer has any faces, deleting.`);
try {
await this.repository.delete(person);
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [person.thumbnailPath] } });
} catch (error: Error | any) {
this.logger.error(`Unable to delete person: ${error}`, error?.stack);
}
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: person.id } });
}
return true;
@@ -167,7 +178,10 @@ export class PersonService {
});
if (force) {
const people = await this.repository.deleteAll();
const people = await this.repository.getAll();
for (const person of people) {
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: person.id } });
}
const faces = await this.searchRepository.deleteAllFaces();
this.logger.debug(`Deleted ${people} people and ${faces} faces`);
}
@@ -363,7 +377,7 @@ export class PersonService {
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId: mergeId } });
}
await this.repository.reassignFaces(mergeData);
await this.repository.delete(mergePerson);
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: mergePerson.id } });
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
results.push({ id: mergeId, success: true });

View File

@@ -235,7 +235,9 @@ export class StorageTemplateService {
filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
};
const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone: asset.exifInfo?.timeZone || undefined });
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const zone = asset.exifInfo?.timeZone || systemTimeZone;
const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone });
const dateTokens = [
...supportedYearTokens,

View File

@@ -91,7 +91,7 @@ export class AssetService {
try {
const libraryId = await this.getLibraryId(authUser, dto.libraryId);
await this.access.requirePermission(authUser, Permission.LIBRARY_WRITE, libraryId);
await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId);
if (livePhotoFile) {
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
@@ -163,7 +163,7 @@ export class AssetService {
try {
const libraryId = await this.getLibraryId(authUser, dto.libraryId);
await this.access.requirePermission(authUser, Permission.LIBRARY_WRITE, libraryId);
await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId);
const asset = await this.assetCore.create(authUser, { ...dto, libraryId }, assetFile, undefined, dto.sidecarPath);
return { id: asset.id, duplicate: false };
} catch (error: QueryFailedError | Error | any) {

View File

@@ -355,7 +355,7 @@ export class AssetRepository implements IAssetRepository {
}
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
const assets = await this.repository.find({
select: {
@@ -368,7 +368,7 @@ export class AssetRepository implements IAssetRepository {
where: {
ownerId,
isVisible: true,
isArchived: false,
isArchived,
exifInfo: {
latitude: Not(IsNull()),
longitude: Not(IsNull()),
@@ -435,7 +435,7 @@ export class AssetRepository implements IAssetRepository {
`SELECT *
FROM assets
WHERE "ownerId" = $1
OFFSET FLOOR(RANDOM() * (SELECT GREATEST(COUNT(*) - $2, 0) FROM ASSETS)) LIMIT $2`,
OFFSET FLOOR(RANDOM() * (SELECT GREATEST(COUNT(*) - $2, 0) FROM ASSETS WHERE "ownerId" = $1)) LIMIT $2`,
[ownerId, count],
);
}

View File

@@ -74,6 +74,7 @@ export class AppService {
[JobName.RECOGNIZE_FACES]: (data) => this.personService.handleRecognizeFaces(data),
[JobName.GENERATE_PERSON_THUMBNAIL]: (data) => this.personService.handleGeneratePersonThumbnail(data),
[JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(),
[JobName.PERSON_DELETE]: (data) => this.personService.handlePersonDelete(data),
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(),

View File

@@ -1,4 +1,11 @@
import { AssetResponseDto, IAssetRepository, IPersonRepository, LoginResponseDto, TimeBucketSize } from '@app/domain';
import {
AssetResponseDto,
IAssetRepository,
IPersonRepository,
LibraryResponseDto,
LoginResponseDto,
TimeBucketSize,
} from '@app/domain';
import { AppModule, AssetController } from '@app/immich';
import { AssetEntity, AssetType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
@@ -56,6 +63,7 @@ const createAsset = (
deviceAssetId: `test_${id}`,
deviceId: 'e2e-test',
libraryId,
isVisible: true,
fileCreatedAt: createdAt,
fileModifiedAt: new Date(),
type: AssetType.IMAGE,
@@ -67,6 +75,7 @@ describe(`${AssetController.name} (e2e)`, () => {
let app: INestApplication;
let server: any;
let assetRepository: IAssetRepository;
let defaultLibrary: LibraryResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let asset1: AssetEntity;
@@ -89,19 +98,25 @@ describe(`${AssetController.name} (e2e)`, () => {
await api.authApi.adminSignUp(server);
const admin = await api.authApi.adminLogin(server);
const libraries = await api.libraryApi.getAll(server, admin.accessToken);
const defaultLibrary = libraries[0];
const [libraries] = await Promise.all([
api.libraryApi.getAll(server, admin.accessToken),
api.userApi.create(server, admin.accessToken, user1Dto),
api.userApi.create(server, admin.accessToken, user2Dto),
]);
await api.userApi.create(server, admin.accessToken, user1Dto);
user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password });
defaultLibrary = libraries[0];
asset1 = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01'));
asset2 = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-02'));
asset3 = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01'));
[user1, user2] = await Promise.all([
api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }),
api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }),
]);
await api.userApi.create(server, admin.accessToken, user2Dto);
user2 = await api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password });
asset4 = await createAsset(assetRepository, user2, defaultLibrary.id, new Date('1970-01-01'));
[asset1, asset2, asset3, asset4] = await Promise.all([
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-02')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
createAsset(assetRepository, user2, defaultLibrary.id, new Date('1970-01-01')),
]);
});
afterAll(async () => {
@@ -378,6 +393,16 @@ describe(`${AssetController.name} (e2e)`, () => {
});
describe('GET /asset/random', () => {
beforeAll(async () => {
await Promise.all([
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')),
]);
});
it('should require authentication', async () => {
const { status, body } = await request(server).get('/asset/random');
@@ -420,6 +445,18 @@ describe(`${AssetController.name} (e2e)`, () => {
}
});
it.each(Array(10))(
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
async () => {
const { status, body } = await request(server)
.get('/[]asset/random')
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
},
);
it('should return error', async () => {
const { status } = await request(server)
.get('/asset/random?count=ABC')
@@ -515,4 +552,45 @@ describe(`${AssetController.name} (e2e)`, () => {
);
});
});
describe('GET /asset/map-marker', () => {
beforeEach(async () => {
await assetRepository.save({ id: asset1.id, isArchived: true });
await assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 });
await assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 });
});
it('should require authentication', async () => {
const { status, body } = await request(server).get('/asset/map-marker');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(server)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: asset1.id }),
expect.objectContaining({ id: asset2.id }),
]),
);
});
it('should get all map markers', async () => {
const { status, body } = await request(server)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ isArchived: false });
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual([expect.objectContaining({ id: asset2.id })]);
});
});
});

View File

@@ -1,5 +1,5 @@
# Our Node base image
FROM node:18.16.0-alpine3.18@sha256:9036ddb8252ba7089c2c83eb2b0dcaf74ff1069e8ddf86fe2bd6dc5fecc9492d as base
FROM node:20.8-alpine3.18 as base
WORKDIR /usr/src/app
EXPOSE 3000

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -6278,13 +6278,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMapMarkers: async (isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getMapMarkers: async (isArchived?: boolean, isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/map-marker`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -6306,6 +6307,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (isArchived !== undefined) {
localVarQueryParameter['isArchived'] = isArchived;
}
if (isFavorite !== undefined) {
localVarQueryParameter['isFavorite'] = isFavorite;
}
@@ -7134,14 +7139,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options);
async getMapMarkers(isArchived?: boolean, isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -7428,7 +7434,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getMapMarkers(requestParameters: AssetApiGetMapMarkersRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<MapMarkerResponseDto>> {
return localVarFp.getMapMarkers(requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(axios, basePath));
return localVarFp.getMapMarkers(requestParameters.isArchived, requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(axios, basePath));
},
/**
*
@@ -7846,6 +7852,13 @@ export interface AssetApiGetDownloadInfoRequest {
* @interface AssetApiGetMapMarkersRequest
*/
export interface AssetApiGetMapMarkersRequest {
/**
*
* @type {boolean}
* @memberof AssetApiGetMapMarkers
*/
readonly isArchived?: boolean
/**
*
* @type {boolean}
@@ -8374,7 +8387,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getMapMarkers(requestParameters: AssetApiGetMapMarkersRequest = {}, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getMapMarkers(requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getMapMarkers(requestParameters.isArchived, requestParameters.isFavorite, requestParameters.fileCreatedAfter, requestParameters.fileCreatedBefore, options).then((request) => request(this.axios, this.basePath));
}
/**

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.80.0
* The version of the OpenAPI document: 1.81.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -32,6 +32,7 @@
>
<SettingSwitch title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch title="Only favorites" bind:checked={settings.onlyFavorites} />
<SettingSwitch title="Include archived" bind:checked={settings.includeArchived} />
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-8">

View File

@@ -43,7 +43,8 @@
let dropdownOpen: boolean[] = [];
let showContextMenu = false;
let contextMenuPosition = { x: 0, y: 0 };
let libraryType: LibraryType;
let selectedLibraryIndex = 0;
let selectedLibrary: LibraryResponseDto | null = null;
onMount(() => {
readLibraryList();
@@ -61,10 +62,12 @@
}
};
const showMenu = (event: MouseEvent, type: LibraryType) => {
const showMenu = (event: MouseEvent, library: LibraryResponseDto, index: number) => {
contextMenuPosition = getContextMenuPosition(event);
showContextMenu = !showContextMenu;
libraryType = type;
selectedLibraryIndex = index;
selectedLibrary = library;
};
const onMenuExit = () => {
@@ -216,54 +219,63 @@
}
};
const onRenameClicked = (index: number) => {
const onRenameClicked = () => {
closeAll();
renameLibrary = index;
updateLibraryIndex = index;
renameLibrary = selectedLibraryIndex;
updateLibraryIndex = selectedLibraryIndex;
};
const onEditImportPathClicked = (index: number) => {
const onEditImportPathClicked = () => {
closeAll();
editImportPaths = index;
updateLibraryIndex = index;
editImportPaths = selectedLibraryIndex;
updateLibraryIndex = selectedLibraryIndex;
};
const onScanNewLibraryClicked = (libraryId: string) => {
const onScanNewLibraryClicked = () => {
closeAll();
handleScan(libraryId);
if (selectedLibrary) {
handleScan(selectedLibrary.id);
}
};
const onScanSettingClicked = (index: number) => {
const onScanSettingClicked = () => {
closeAll();
editScanSettings = index;
updateLibraryIndex = index;
editScanSettings = selectedLibraryIndex;
updateLibraryIndex = selectedLibraryIndex;
};
const onScanAllLibraryFilesClicked = (libraryId: string) => {
const onScanAllLibraryFilesClicked = () => {
closeAll();
handleScanChanges(libraryId);
if (selectedLibrary) {
handleScanChanges(selectedLibrary.id);
}
};
const onForceScanAllLibraryFilesClicked = (libraryId: string) => {
const onForceScanAllLibraryFilesClicked = () => {
closeAll();
handleForceScan(libraryId);
if (selectedLibrary) {
handleForceScan(selectedLibrary.id);
}
};
const onRemoveOfflineFilesClicked = (libraryId: string) => {
const onRemoveOfflineFilesClicked = () => {
closeAll();
handleRemoveOffline(libraryId);
if (selectedLibrary) {
handleRemoveOffline(selectedLibrary.id);
}
};
const onDeleteLibraryClicked = (index: number, library: LibraryResponseDto) => {
const onDeleteLibraryClicked = () => {
closeAll();
if (confirm(`Are you sure you want to delete ${library.name} library?`) == true) {
refreshStats(index);
if (totalCount[index] > 0) {
deleteAssetCount = totalCount[index];
confirmDeleteLibrary = library;
if (selectedLibrary && confirm(`Are you sure you want to delete ${selectedLibrary.name} library?`) == true) {
refreshStats(selectedLibraryIndex);
if (totalCount[selectedLibraryIndex] > 0) {
deleteAssetCount = totalCount[selectedLibraryIndex];
confirmDeleteLibrary = selectedLibrary;
} else {
deleteLibrary = library;
deleteLibrary = selectedLibrary;
handleDelete();
}
}
@@ -295,104 +307,92 @@
</tr>
</thead>
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each libraries as library, index}
{#key library.id}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
index % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
{#each libraries as library, index (library.id)}
<tr
class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
index % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="w-1/6 px-10 text-sm">
{#if library.type === LibraryType.External}
<Database size="40" title="External library (created on {library.createdAt})" />
{:else if library.type === LibraryType.Upload}
<Upload size="40" title="Upload library (created on {library.createdAt})" />
{/if}</td
>
<td class="w-1/6 px-10 text-sm">
{#if library.type === LibraryType.External}
<Database size="40" title="External library (created on {library.createdAt})" />
{:else if library.type === LibraryType.Upload}
<Upload size="40" title="Upload library (created on {library.createdAt})" />
{/if}</td
>
<td class="w-1/3 text-ellipsis px-4 text-sm">{library.name}</td>
{#if totalCount[index] == undefined}
<td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
<Pulse color="gray" size="40" unit="px" />
</td>
{:else}
<td class="w-1/6 text-ellipsis px-4 text-sm">
{totalCount[index]}
</td>
<td class="w-1/6 text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]} </td>
{/if}
<td class="w-1/6 text-ellipsis px-4 text-sm">
<button
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library.type)}
>
<DotsVertical size="16" />
</button>
{#if showContextMenu}
<Portal target="body">
<ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
<MenuOption on:click={() => onRenameClicked(index)} text="Rename" />
{#if libraryType === LibraryType.External}
<MenuOption on:click={() => onEditImportPathClicked(index)} text="Edit Import Paths" />
<MenuOption on:click={() => onScanSettingClicked(index)} text="Scan Settings" />
<hr />
<MenuOption
on:click={() => onScanNewLibraryClicked(library.id)}
text="Scan New Library Files"
/>
<MenuOption
on:click={() => onScanAllLibraryFilesClicked(library.id)}
text="Re-scan All Library Files"
subtitle={'Only refreshes modified files'}
/>
<MenuOption
on:click={() => onForceScanAllLibraryFilesClicked(library.id)}
text="Force Re-scan All Library Files"
subtitle={'Refreshes every file'}
/>
<hr />
<MenuOption
on:click={() => onRemoveOfflineFilesClicked(library.id)}
text="Remove Offline Files"
/>
<MenuOption on:click={() => onDeleteLibraryClicked(index, library)}>
<p class="text-red-600">Delete library</p>
</MenuOption>
{/if}
</ContextMenu>
</Portal>
{/if}
<td class="w-1/3 text-ellipsis px-4 text-sm">{library.name}</td>
{#if totalCount[index] == undefined}
<td colspan="2" class="flex w-1/3 items-center justify-center text-ellipsis px-4 text-sm">
<Pulse color="gray" size="40" unit="px" />
</td>
</tr>
{#if renameLibrary === index}
<div transition:slide={{ duration: 250 }}>
<LibraryRenameForm {library} on:submit={handleUpdate} on:cancel={() => (renameLibrary = null)} />
</div>
{:else}
<td class="w-1/6 text-ellipsis px-4 text-sm">
{totalCount[index]}
</td>
<td class="w-1/6 text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]}</td>
{/if}
{#if editImportPaths === index}
<div transition:slide={{ duration: 250 }}>
<LibraryImportPathsForm
{library}
on:submit={handleUpdate}
on:cancel={() => (editImportPaths = null)}
/>
</div>
{/if}
{#if editScanSettings === index}
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
<LibraryScanSettingsForm
{library}
on:submit={handleUpdate}
on:cancel={() => (editScanSettings = null)}
/>
</div>
{/if}
{/key}
<td class="w-1/6 text-ellipsis px-4 text-sm">
<button
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library, index)}
>
<DotsVertical size="16" />
</button>
{#if showContextMenu}
<Portal target="body">
<ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
<MenuOption on:click={() => onRenameClicked()} text={`Rename`} />
{#if selectedLibrary && selectedLibrary.type === LibraryType.External}
<MenuOption on:click={() => onEditImportPathClicked()} text="Edit Import Paths" />
<MenuOption on:click={() => onScanSettingClicked()} text="Scan Settings" />
<hr />
<MenuOption on:click={() => onScanNewLibraryClicked()} text="Scan New Library Files" />
<MenuOption
on:click={() => onScanAllLibraryFilesClicked()}
text="Re-scan All Library Files"
subtitle={'Only refreshes modified files'}
/>
<MenuOption
on:click={() => onForceScanAllLibraryFilesClicked()}
text="Force Re-scan All Library Files"
subtitle={'Refreshes every file'}
/>
<hr />
<MenuOption on:click={() => onRemoveOfflineFilesClicked()} text="Remove Offline Files" />
<MenuOption on:click={() => onDeleteLibraryClicked()}>
<p class="text-red-600">Delete library</p>
</MenuOption>
{/if}
</ContextMenu>
</Portal>
{/if}
</td>
</tr>
{#if renameLibrary === index}
<div transition:slide={{ duration: 250 }}>
<LibraryRenameForm {library} on:submit={handleUpdate} on:cancel={() => (renameLibrary = null)} />
</div>
{/if}
{#if editImportPaths === index}
<div transition:slide={{ duration: 250 }}>
<LibraryImportPathsForm {library} on:submit={handleUpdate} on:cancel={() => (editImportPaths = null)} />
</div>
{/if}
{#if editScanSettings === index}
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
<LibraryScanSettingsForm
{library}
on:submit={handleUpdate}
on:cancel={() => (editScanSettings = null)}
/>
</div>
{/if}
{/each}
</tbody>
</table>

View File

@@ -21,6 +21,7 @@ export const locale = persisted<string | undefined>('locale', undefined, {
export interface MapSettings {
allowDarkMode: boolean;
includeArchived: boolean;
onlyFavorites: boolean;
relativeDate: string;
dateAfter: string;
@@ -29,6 +30,7 @@ export interface MapSettings {
export const mapSettings = persisted<MapSettings>('map-settings', {
allowDarkMode: true,
includeArchived: false,
onlyFavorites: false,
relativeDate: '',
dateAfter: '',

View File

@@ -44,11 +44,12 @@
}
abortController = new AbortController();
const { onlyFavorites } = $mapSettings;
const { includeArchived, onlyFavorites } = $mapSettings;
const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
const { data } = await api.assetApi.getMapMarkers(
{
isArchived: includeArchived && undefined,
isFavorite: onlyFavorites || undefined,
fileCreatedAfter: fileCreatedAfter || undefined,
fileCreatedBefore,