Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
126dd45751 | ||
|
|
ff331ffad9 | ||
|
|
e571880c16 | ||
|
|
e5b4d09827 | ||
|
|
81d51fbd7e | ||
|
|
02f9b40d67 | ||
|
|
260a600bbc | ||
|
|
818005fcb5 | ||
|
|
e5f704cf3b | ||
|
|
e2f1e38472 | ||
|
|
b3c82d5ba2 | ||
|
|
6d1868a6e0 | ||
|
|
98db9331d8 |
25
cli/src/api/open-api/api.ts
generated
25
cli/src/api/open-api/api.ts
generated
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
@@ -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).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
@@ -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).
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
;
|
||||
|
||||
@@ -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(
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -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
|
||||
|
||||
6
mobile/openapi/doc/AssetApi.md
generated
6
mobile/openapi/doc/AssetApi.md
generated
@@ -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]
|
||||
|
||||
13
mobile/openapi/lib/api/asset_api.dart
generated
13
mobile/openapi/lib/api/asset_api.dart
generated
@@ -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));
|
||||
}
|
||||
|
||||
2
mobile/openapi/test/asset_api_test.dart
generated
2
mobile/openapi/test/asset_api_test.dart
generated
@@ -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
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.80.0",
|
||||
"version": "1.81.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface LivePhotoSearchOptions {
|
||||
}
|
||||
|
||||
export interface MapMarkerSearchOptions {
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
fileCreatedBefore?: Date;
|
||||
fileCreatedAfter?: Date;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 })]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
25
web/src/api/open-api/api.ts
generated
25
web/src/api/open-api/api.ts
generated
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -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).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -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).
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user