diff --git a/cli/Dockerfile b/cli/Dockerfile index 31dd8576e2..da2f17cc39 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS core +FROM node:22.13.0-alpine3.20@sha256:db8dcb90326a0116375414e9a7c068a6b87a4422b7da37b5c6cd026f7c7835d3 AS core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index cb7943646c..b655c6e4b0 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -71,6 +71,7 @@ services: - ../web:/usr/src/app - ../i18n:/usr/src/i18n - ../open-api/:/usr/src/open-api/ + # - ../../ui:/usr/ui - /usr/src/app/node_modules ulimits: nofile: diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 9dbaf157b5..f341c3e9cb 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -63,6 +63,17 @@ If you only want to do web development connected to an existing, remote backend, IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev ``` +#### `@immich/ui` + +To see local changes to `@immich/ui` in Immich, do the following: + +1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui` +1. Build the `@immich/ui` project via `npm run build` +1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) +1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) +1. Start up the stack via `make dev` +1. After making changes in `@immich/ui`, rebuild it (`npm run build`) + ### Mobile app The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system. diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 4193cc971c..9bfb04e6b7 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -148,26 +148,29 @@ Redis (Sentinel) URL example JSON before encoding: ## Machine Learning -| Variable | Description | Default | Containers | -| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- | -| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | -| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | -| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | -| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | -| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | -| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | -| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | -| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning | -| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning | -| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | -| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | -| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | -| `MACHINE_LEARNING_DEVICE_IDS`\*4 | Device IDs to use in multi-GPU environments | `0` | machine learning | -| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | -| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning | -| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning | + +| Variable | Description | Default | Containers | +| :---------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- | +| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | +| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | +| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | +| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | +| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | +| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | +| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning | +| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Name of the textual CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Name of the visual CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Name of the recognition portion of the facial recognition model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Name of the detection portion of the facial recognition model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | +| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | +| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | +| `MACHINE_LEARNING_DEVICE_IDS`\*4 | Device IDs to use in multi-GPU environments | `0` | machine learning | +| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | +| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning | +| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning | \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 42989a118f..9cd5f0252a 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -13,8 +13,8 @@ import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; const authServer = { - internal: 'http://auth-server:3000', - external: 'http://127.0.0.1:3000', + internal: 'http://auth-server:2286', + external: 'http://127.0.0.1:2286', }; const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect'; diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts index cde50813dd..575e97d291 100644 --- a/e2e/src/setup/auth-server.ts +++ b/e2e/src/setup/auth-server.ts @@ -51,7 +51,7 @@ const setup = async () => { const { privateKey, publicKey } = await generateKeyPair('RS256'); const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect']; - const port = 3000; + const port = 2286; const host = '0.0.0.0'; const oidc = new Provider(`http://${host}:${port}`, { renderError: async (ctx, out, error) => { diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 4116f5f7b7..2b238555c0 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:b337e1fd27dbacda505219f713789bf82766694095876769ea10c2d34b4f470b AS builder-cpu +FROM python:3.11-bookworm@sha256:f997d3f71b7dcff3f937703c02861437f2b41a94e1ddbd1b5fa357ee99f5cce4 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -36,7 +36,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:873952659a04188d2a62d5f7e30fd673d2559432a847a8ad5fcaf9cbd085e9ed AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/app/config.py b/machine-learning/app/config.py index 932e12a230..c9816d98c6 100644 --- a/machine-learning/app/config.py +++ b/machine-learning/app/config.py @@ -14,9 +14,29 @@ from uvicorn import Server from uvicorn.workers import UvicornWorker +class ClipSettings(BaseModel): + textual: str | None = None + visual: str | None = None + + +class FacialRecognitionSettings(BaseModel): + recognition: str | None = None + detection: str | None = None + + class PreloadModelData(BaseModel): - clip: str | None = None - facial_recognition: str | None = None + clip_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__CLIP", None) + facial_recognition_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION", None) + if clip_fallback is not None: + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = clip_fallback + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = clip_fallback + del os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] + if facial_recognition_fallback is not None: + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = facial_recognition_fallback + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = facial_recognition_fallback + del os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] + clip: ClipSettings = ClipSettings() + facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings() class MaxBatchSize(BaseModel): diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index 684001b875..fb6f84499a 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -75,21 +75,46 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: async def preload_models(preload: PreloadModelData) -> None: - log.info(f"Preloading models: {preload}") - if preload.clip is not None: - model = await model_cache.get(preload.clip, ModelType.TEXTUAL, ModelTask.SEARCH) + log.info(f"Preloading models: clip:{preload.clip} facial_recognition:{preload.facial_recognition}") + + if preload.clip.textual is not None: + model = await model_cache.get(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH) await load(model) - model = await model_cache.get(preload.clip, ModelType.VISUAL, ModelTask.SEARCH) + if preload.clip.visual is not None: + model = await model_cache.get(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH) await load(model) - if preload.facial_recognition is not None: - model = await model_cache.get(preload.facial_recognition, ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION) + if preload.facial_recognition.detection is not None: + model = await model_cache.get( + preload.facial_recognition.detection, + ModelType.DETECTION, + ModelTask.FACIAL_RECOGNITION, + ) await load(model) - model = await model_cache.get(preload.facial_recognition, ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION) + if preload.facial_recognition.recognition is not None: + model = await model_cache.get( + preload.facial_recognition.recognition, + ModelType.RECOGNITION, + ModelTask.FACIAL_RECOGNITION, + ) await load(model) + if preload.clip_fallback is not None: + log.warning( + "Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__CLIP'. " + "Use 'MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL' and " + "'MACHINE_LEARNING_PRELOAD__CLIP__VISUAL' instead." + ) + + if preload.facial_recognition_fallback is not None: + log.warning( + "Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION'. " + "Use 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION' and " + "'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION' instead." + ) + def update_state() -> Iterator[None]: global active_requests, last_called diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index 5ca0c4598e..8d63d63c76 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -750,11 +750,13 @@ class TestCache: await model_cache.get("test_model_name", ModelType.TEXTUAL, ModelTask.SEARCH) async def test_preloads_clip_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None: - os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = "ViT-B-32__openai" settings = Settings() assert settings.preload is not None - assert settings.preload.clip == "ViT-B-32__openai" + assert settings.preload.clip.textual == "ViT-B-32__openai" + assert settings.preload.clip.visual == "ViT-B-32__openai" model_cache = ModelCache() monkeypatch.setattr("app.main.model_cache", model_cache) @@ -771,11 +773,13 @@ class TestCache: async def test_preloads_facial_recognition_models( self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock ) -> None: - os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = "buffalo_s" settings = Settings() assert settings.preload is not None - assert settings.preload.facial_recognition == "buffalo_s" + assert settings.preload.facial_recognition.detection == "buffalo_s" + assert settings.preload.facial_recognition.recognition == "buffalo_s" model_cache = ModelCache() monkeypatch.setattr("app.main.model_cache", model_cache) @@ -790,13 +794,17 @@ class TestCache: ) async def test_preloads_all_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None: - os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai" - os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = "buffalo_s" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = "buffalo_s" settings = Settings() assert settings.preload is not None - assert settings.preload.clip == "ViT-B-32__openai" - assert settings.preload.facial_recognition == "buffalo_s" + assert settings.preload.clip.visual == "ViT-B-32__openai" + assert settings.preload.clip.textual == "ViT-B-32__openai" + assert settings.preload.facial_recognition.recognition == "buffalo_s" + assert settings.preload.facial_recognition.detection == "buffalo_s" model_cache = ModelCache() monkeypatch.setattr("app.main.model_cache", model_cache) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 37886d9a30..dd25d59cf7 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2498,13 +2498,13 @@ files = [ [[package]] name = "pydantic" -version = "2.10.4" +version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, + {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, + {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, ] [package.dependencies] diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index e49cf5b8da..ac57884eef 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -43,7 +43,7 @@ android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon" /> - @@ -61,6 +61,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -96,4 +124,4 @@ - + \ No newline at end of file diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index c0d23c681d..b85d52baa9 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -657,5 +657,15 @@ "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack", "wifi_name": "WiFi Name", - "your_wifi_name": "Your WiFi name" + "your_wifi_name": "Your WiFi name", + "upload": "Upload", + "uploading": "Uploading", + "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "enqueued": "Enqueued", + "not_selected": "Not selected", + "completed": "Completed", + "failed": "Failed", + "paused": "Paused", + "canceled": "Canceled", + "upload_to_immich": "Upload to Immich ({})" } diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index b048c0bb0c..a98032db20 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -32,6 +32,13 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + # share_handler addition start + target 'ShareExtension' do + inherit! :search_paths + pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models" + end + # share_handler addition end end post_install do |installer| diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index bc65bd4b7f..00a63be8d7 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -82,9 +82,17 @@ PODS: - Flutter - FlutterMacOS - SAMKeychain (1.5.3) - - SDWebImage (5.19.4): - - SDWebImage/Core (= 5.19.4) - - SDWebImage/Core (5.19.4) + - SDWebImage (5.20.0): + - SDWebImage/Core (= 5.20.0) + - SDWebImage/Core (5.20.0) + - share_handler_ios (0.0.14): + - Flutter + - share_handler_ios/share_handler_ios_models (= 0.0.14) + - share_handler_ios_models + - share_handler_ios/share_handler_ios_models (0.0.14): + - Flutter + - share_handler_ios_models + - share_handler_ios_models (0.0.9) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -94,7 +102,7 @@ PODS: - Flutter - FlutterMacOS - SwiftyGif (5.4.5) - - Toast (4.0.0) + - Toast (4.1.1) - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -123,6 +131,8 @@ DEPENDENCIES: - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) + - share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`) + - share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) @@ -184,6 +194,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" + share_handler_ios: + :path: ".symlinks/plugins/share_handler_ios/ios" + share_handler_ios_models: + :path: ".symlinks/plugins/share_handler_ios/ios/Models" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -222,15 +236,17 @@ SPEC CHECKSUMS: permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d + SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 + share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c + share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 -PODFILE CHECKSUM: 2282844f7aed70427ae663932332dad1225156c8 +PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c COCOAPODS: 1.15.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index b3da30f108..10133cc330 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; }; 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -16,8 +17,21 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; + FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; }; + FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + FAC6F8982D287C890078CB2F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = FAC6F88F2D287C890078CB2F; + remoteInfo = ShareExtension; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -29,13 +43,26 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; @@ -49,9 +76,16 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FAC6F8B22D287F120078CB2F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; + FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ @@ -64,6 +98,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FAC6F88D2D287C890078CB2F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -73,6 +115,9 @@ 2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */, E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */, F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */, + F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */, + 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */, + B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -81,6 +126,7 @@ isa = PBXGroup; children = ( 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */, + 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -110,6 +156,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + FAC6F8B62D287F120078CB2F /* ShareExtension */, 97C146EF1CF9000F007C117D /* Products */, 0FB772A5B9601143383626CA /* Pods */, 1754452DD81DA6620E279E51 /* Frameworks */, @@ -120,6 +167,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Immich-Debug.app */, + FAC6F8902D287C890078CB2F /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -142,6 +190,17 @@ path = Runner; sourceTree = ""; }; + FAC6F8B62D287F120078CB2F /* ShareExtension */ = { + isa = PBXGroup; + children = ( + FAC6F8B12D287F120078CB2F /* Info.plist */, + FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */, + FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */, + FAC6F8B52D287F120078CB2F /* ShareViewController.swift */, + ); + path = ShareExtension; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -155,6 +214,7 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, + FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */, 6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */, @@ -162,12 +222,31 @@ buildRules = ( ); dependencies = ( + FAC6F8992D287C890078CB2F /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; productType = "com.apple.product-type.application"; }; + FAC6F88F2D287C890078CB2F /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */, + FAC6F88C2D287C890078CB2F /* Sources */, + FAC6F88D2D287C890078CB2F /* Frameworks */, + FAC6F88E2D287C890078CB2F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + productName = ShareExtension; + productReference = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -175,6 +254,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -182,6 +262,9 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + FAC6F88F2D287C890078CB2F = { + CreatedOnToolsVersion = 16.0; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -198,6 +281,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + FAC6F88F2D287C890078CB2F /* ShareExtension */, ); }; /* End PBXProject section */ @@ -214,6 +298,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FAC6F88E2D287C890078CB2F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -233,6 +325,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -318,8 +432,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FAC6F88C2D287C890078CB2F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + FAC6F8992D287C890078CB2F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FAC6F88F2D287C890078CB2F /* ShareExtension */; + targetProxy = FAC6F8982D287C890078CB2F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -337,6 +467,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + FAC6F8B22D287F120078CB2F /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -404,6 +542,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 187; + CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -547,6 +686,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 187; + CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -576,6 +716,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 187; + CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -594,6 +735,129 @@ }; name = Release; }; + FAC6F89C2D287C890078CB2F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.app.immich.share; + DEVELOPMENT_TEAM = 2F67MQ8R79; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FAC6F89D2D287C890078CB2F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.app.immich.share; + DEVELOPMENT_TEAM = 2F67MQ8R79; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FAC6F89E2D287C890078CB2F /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.app.immich.share; + DEVELOPMENT_TEAM = 2F67MQ8R79; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -617,6 +881,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FAC6F89C2D287C890078CB2F /* Debug */, + FAC6F89D2D287C890078CB2F /* Release */, + FAC6F89E2D287C890078CB2F /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index f3aed115b2..be5ec5d9d7 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + AppGroupId + $(CUSTOM_GROUP_ID) BGTaskSchedulerPermittedIdentifiers app.alextran.immich.backgroundFetch @@ -13,6 +15,24 @@ $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName ${PRODUCT_NAME} + CFBundleDocumentTypes + + + CFBundleTypeName + ShareHandler + LSHandlerRank + Alternate + LSItemContentTypes + + public.file-url + public.image + public.text + public.movie + public.url + public.data + + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -61,6 +81,17 @@ 1.124.0 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + CFBundleVersion 187 FLTEnableImpeller @@ -73,6 +104,8 @@ LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + No MGLMapboxMetricsEnabledSettingShownInApp NSAppTransportSecurity @@ -94,6 +127,10 @@ We need to manage backup your photos album NSPhotoLibraryUsageDescription We need to manage backup your photos album + NSUserActivityTypes + + INSendMessageIntent + UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements index ba21fbdaf2..d558e35e0a 100644 --- a/mobile/ios/Runner/Runner.entitlements +++ b/mobile/ios/Runner/Runner.entitlements @@ -4,5 +4,9 @@ com.apple.developer.networking.wifi-info + com.apple.security.application-groups + + group.app.immich.share + diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements index 75e36a143e..d44633db40 100644 --- a/mobile/ios/Runner/RunnerProfile.entitlements +++ b/mobile/ios/Runner/RunnerProfile.entitlements @@ -6,5 +6,9 @@ development com.apple.developer.networking.wifi-info + com.apple.security.application-groups + + group.app.immich.share + diff --git a/mobile/ios/ShareExtension/Base.lproj/MainInterface.storyboard b/mobile/ios/ShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000000..286a50894d --- /dev/null +++ b/mobile/ios/ShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/ShareExtension/Info.plist b/mobile/ios/ShareExtension/Info.plist new file mode 100644 index 0000000000..0f52fbffdf --- /dev/null +++ b/mobile/ios/ShareExtension/Info.plist @@ -0,0 +1,35 @@ + + + + + AppGroupId + $(CUSTOM_GROUP_ID) + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + NSExtensionActivationRule + SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, + $attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY + $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY + $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY + $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count > 0 + ).@count > 0 + PHSupportedMediaTypes + + Video + Image + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + \ No newline at end of file diff --git a/mobile/ios/ShareExtension/ShareExtension.entitlements b/mobile/ios/ShareExtension/ShareExtension.entitlements new file mode 100644 index 0000000000..4ad1a257d8 --- /dev/null +++ b/mobile/ios/ShareExtension/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.app.immich.share + + + diff --git a/mobile/ios/ShareExtension/ShareViewController.swift b/mobile/ios/ShareExtension/ShareViewController.swift new file mode 100644 index 0000000000..b1b38efc79 --- /dev/null +++ b/mobile/ios/ShareExtension/ShareViewController.swift @@ -0,0 +1,3 @@ +import share_handler_ios_models + +class ShareViewController: ShareHandlerIosViewController {} \ No newline at end of file diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 8b74b1a66f..cc0e7ca215 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1 +1,3 @@ const int noDbId = -9223372036854775808; // from Isar +const double downloadCompleted = -1; +const double downloadFailed = -2; diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart index bdf11f18de..cabf2dee53 100644 --- a/mobile/lib/interfaces/album.interface.dart +++ b/mobile/lib/interfaces/album.interface.dart @@ -13,6 +13,7 @@ abstract interface class IAlbumRepository implements IDatabaseRepository { String name, { bool? shared, bool? remote, + bool? owner, }); Future> getAll({ diff --git a/mobile/lib/interfaces/share_handler.interface.dart b/mobile/lib/interfaces/share_handler.interface.dart new file mode 100644 index 0000000000..6d0eb9170c --- /dev/null +++ b/mobile/lib/interfaces/share_handler.interface.dart @@ -0,0 +1,7 @@ +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; + +abstract interface class IShareHandlerRepository { + void Function(List)? onSharedMedia; + + Future init(); +} diff --git a/mobile/lib/interfaces/upload.interface.dart b/mobile/lib/interfaces/upload.interface.dart new file mode 100644 index 0000000000..d4b2298a14 --- /dev/null +++ b/mobile/lib/interfaces/upload.interface.dart @@ -0,0 +1,11 @@ +import 'package:background_downloader/background_downloader.dart'; + +abstract interface class IUploadRepository { + void Function(TaskStatusUpdate)? onUploadStatus; + void Function(TaskProgressUpdate)? onTaskProgress; + + Future upload(UploadTask task); + Future cancel(String id); + Future deleteAllTrackingRecords(); + Future deleteRecordsWithIds(List id); +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 807212fc65..139366b359 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:timezone/data/latest.dart'; import 'package:isar/isar.dart'; @@ -107,10 +108,12 @@ Future initApp() async { progressBar: true, ); - FileDownloader().trackTasksInGroup( + await FileDownloader().trackTasksInGroup( downloadGroupLivePhoto, markDownloadedComplete: false, ); + + await FileDownloader().trackTasks(); } Future loadDb() async { @@ -208,6 +211,8 @@ class ImmichAppState extends ConsumerState // needs to be delayed so that EasyLocalization is working ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); }); + + ref.read(shareIntentUploadProvider.notifier).init(); } @override diff --git a/mobile/lib/models/upload/share_intent_attachment.model.dart b/mobile/lib/models/upload/share_intent_attachment.model.dart new file mode 100644 index 0000000000..1bdb5b6b48 --- /dev/null +++ b/mobile/lib/models/upload/share_intent_attachment.model.dart @@ -0,0 +1,114 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; +import 'dart:io'; + +import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:path/path.dart'; + +enum ShareIntentAttachmentType { + image, + video, +} + +enum UploadStatus { + enqueued, + running, + complete, + notFound, + failed, + canceled, + waitingtoRetry, + paused, +} + +class ShareIntentAttachment { + final String path; + + // enum + final ShareIntentAttachmentType type; + + // enum + final UploadStatus status; + + final double uploadProgress; + + final int fileLength; + + ShareIntentAttachment({ + required this.path, + required this.type, + required this.status, + this.uploadProgress = 0, + this.fileLength = 0, + }); + + int get id => hash(path); + + File get file => File(path); + + String get fileName => basename(file.path); + + bool get isImage => type == ShareIntentAttachmentType.image; + + bool get isVideo => type == ShareIntentAttachmentType.video; + + String? _fileSize; + + String get fileSize => _fileSize ??= formatHumanReadableBytes(fileLength, 2); + + ShareIntentAttachment copyWith({ + String? path, + ShareIntentAttachmentType? type, + UploadStatus? status, + double? uploadProgress, + }) { + return ShareIntentAttachment( + path: path ?? this.path, + type: type ?? this.type, + status: status ?? this.status, + uploadProgress: uploadProgress ?? this.uploadProgress, + ); + } + + Map toMap() { + return { + 'path': path, + 'type': type.index, + 'status': status.index, + 'uploadProgress': uploadProgress, + }; + } + + factory ShareIntentAttachment.fromMap(Map map) { + return ShareIntentAttachment( + path: map['path'] as String, + type: ShareIntentAttachmentType.values[map['type'] as int], + status: UploadStatus.values[map['status'] as int], + uploadProgress: map['uploadProgress'] as double, + ); + } + + String toJson() => json.encode(toMap()); + + factory ShareIntentAttachment.fromJson(String source) => + ShareIntentAttachment.fromMap( + json.decode(source) as Map, + ); + + @override + String toString() { + return 'ShareIntentAttachment(path: $path, type: $type, status: $status, uploadProgress: $uploadProgress)'; + } + + @override + bool operator ==(covariant ShareIntentAttachment other) { + if (identical(this, other)) return true; + + return other.path == path && other.type == type; + } + + @override + int get hashCode { + return path.hashCode ^ type.hashCode; + } +} diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart index 4cb9804e25..f417f9fb38 100644 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ b/mobile/lib/pages/album/album_shared_user_icons.dart @@ -32,7 +32,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget { } return GestureDetector( - onTap: () => context.pushRoute(AlbumOptionsRoute()), + onTap: () => context.pushRoute(const AlbumOptionsRoute()), child: SizedBox( height: 50, child: ListView.builder( diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart index c6bbeb2e7d..f2cb9f19ae 100644 --- a/mobile/lib/pages/common/large_leading_tile.dart +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -13,6 +13,9 @@ class LargeLeadingTile extends StatelessWidget { horizontal: 16.0, ), this.borderRadius = 20.0, + this.trailing, + this.selected = false, + this.disabled = false, }); final Widget leading; @@ -21,30 +24,43 @@ class LargeLeadingTile extends StatelessWidget { final Widget? subtitle; final EdgeInsetsGeometry leadingPadding; final double borderRadius; - + final Widget? trailing; + final bool selected; + final bool disabled; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(borderRadius), - onTap: onTap, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: leadingPadding, - child: leading, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: context.width * 0.6, - child: title, + onTap: disabled ? null : onTap, + child: Container( + decoration: BoxDecoration( + color: selected + ? Theme.of(context).primaryColor.withAlpha(30) + : Colors.transparent, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: leadingPadding, + child: leading, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: context.width * 0.6, + child: title, + ), + subtitle ?? const SizedBox.shrink(), + ], ), - subtitle ?? const SizedBox.shrink(), - ], - ), - ], + ), + if (trailing != null) trailing!, + ], + ), ), ); } diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index 30fe1ab3f2..9e15b0193e 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -34,10 +34,12 @@ class PhotosPage extends HookConsumerWidget { Future(() => ref.read(assetProvider.notifier).getAllAsset()); Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums()); ref.read(serverInfoProvider.notifier).getServerInfo(); + return; }, [], ); + Widget buildLoadingIndicator() { Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart new file mode 100644 index 0000000000..56d093b761 --- /dev/null +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -0,0 +1,263 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; + +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; +import 'package:immich_mobile/entities/store.entity.dart' as db_store; + +@RoutePage() +class ShareIntentPage extends HookConsumerWidget { + const ShareIntentPage({super.key, required this.attachments}); + + final List attachments; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentEndpoint = + db_store.Store.get(db_store.StoreKey.serverEndpoint); + final candidates = ref.watch(shareIntentUploadProvider); + final isUploaded = useState(false); + + void removeAttachment(ShareIntentAttachment attachment) { + ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment); + } + + void addAttachments(List attachments) { + ref.read(shareIntentUploadProvider.notifier).addAttachments(attachments); + } + + void upload() async { + for (final attachment in candidates) { + await ref + .read(shareIntentUploadProvider.notifier) + .upload(attachment.file); + } + + isUploaded.value = true; + } + + bool isSelected(ShareIntentAttachment attachment) { + return candidates.contains(attachment); + } + + void toggleSelection(ShareIntentAttachment attachment) { + if (isSelected(attachment)) { + removeAttachment(attachment); + } else { + addAttachments([attachment]); + } + } + + return Scaffold( + appBar: AppBar( + title: Column( + children: [ + const Text('upload_to_immich').tr( + args: [ + candidates.length.toString(), + ], + ), + Text( + currentEndpoint, + style: context.textTheme.labelMedium?.copyWith( + color: context.colorScheme.onSurface.withAlpha(200), + ), + ), + ], + ), + ), + body: ListView.builder( + itemCount: attachments.length, + itemBuilder: (context, index) { + final attachment = attachments[index]; + final target = candidates.firstWhere( + (element) => element.id == attachment.id, + orElse: () => attachment, + ); + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 16, + ), + child: LargeLeadingTile( + onTap: () => toggleSelection(attachment), + disabled: isUploaded.value, + selected: isSelected(attachment), + leading: Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: attachment.isImage + ? Image.file( + attachment.file, + width: 64, + height: 64, + fit: BoxFit.cover, + ) + : const SizedBox( + width: 64, + height: 64, + child: Center( + child: Icon( + Icons.videocam, + color: Colors.white, + ), + ), + ), + ), + if (attachment.isImage) + const Positioned( + top: 8, + right: 8, + child: Icon( + Icons.image, + color: Colors.white, + size: 20, + shadows: [ + Shadow( + offset: Offset(0, 0), + blurRadius: 8.0, + color: Colors.black45, + ), + ], + ), + ), + ], + ), + title: Text( + attachment.fileName, + style: context.textTheme.titleSmall, + ), + subtitle: Text( + attachment.fileSize, + style: context.textTheme.labelLarge, + ), + trailing: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: UploadStatusIcon( + selected: isSelected(attachment), + status: target.status, + progress: target.uploadProgress, + ), + ), + ), + ); + }, + ), + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + height: 48, + child: ElevatedButton( + onPressed: isUploaded.value ? null : upload, + child: isUploaded.value + ? UploadingText(candidates: candidates) + : const Text('upload').tr(), + ), + ), + ), + ), + ); + } +} + +class UploadingText extends StatelessWidget { + const UploadingText({super.key, required this.candidates}); + final List candidates; + + @override + Widget build(BuildContext context) { + final uploadedCount = candidates.where((element) { + return element.status == UploadStatus.complete; + }).length; + + return const Text("shared_intent_upload_button_progress_text") + .tr(args: [uploadedCount.toString(), candidates.length.toString()]); + } +} + +class UploadStatusIcon extends StatelessWidget { + const UploadStatusIcon({ + super.key, + required this.status, + required this.selected, + this.progress = 0, + }); + + final UploadStatus status; + final double progress; + final bool selected; + + @override + Widget build(BuildContext context) { + if (!selected) { + return Icon( + Icons.check_circle_outline_rounded, + color: context.colorScheme.onSurface.withAlpha(100), + semanticLabel: 'not_selected'.tr(), + ); + } + + final statusIcon = switch (status) { + UploadStatus.enqueued => Icon( + Icons.check_circle_rounded, + color: context.primaryColor, + semanticLabel: 'enqueued'.tr(), + ), + UploadStatus.running => Stack( + alignment: AlignmentDirectional.center, + children: [ + SizedBox( + width: 40, + height: 40, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: progress), + duration: const Duration(milliseconds: 500), + builder: (context, value, _) => CircularProgressIndicator( + backgroundColor: context.colorScheme.surfaceContainerLow, + strokeWidth: 3, + value: value, + semanticsLabel: 'uploading'.tr(), + ), + ), + ), + Text( + (progress * 100).toStringAsFixed(0), + style: context.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + UploadStatus.complete => Icon( + Icons.check_circle_rounded, + color: Colors.green, + semanticLabel: 'completed'.tr(), + ), + UploadStatus.notFound || UploadStatus.failed => Icon( + Icons.error_rounded, + color: Colors.red, + semanticLabel: 'failed'.tr(), + ), + UploadStatus.canceled => Icon( + Icons.cancel_rounded, + color: Colors.red, + semanticLabel: 'canceled'.tr(), + ), + UploadStatus.waitingtoRetry || UploadStatus.paused => Icon( + Icons.pause_circle_rounded, + color: context.primaryColor, + semanticLabel: 'paused'.tr(), + ), + }; + + return statusIcon; + } +} diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index b3d619a815..8c06faaa6a 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -46,8 +46,18 @@ class AlbumNotifier extends StateNotifier> { ) => _albumService.createAlbum(albumTitle, assets, []); - Future getAlbumByName(String albumName, {bool remoteOnly = false}) => - _albumService.getAlbumByName(albumName, remoteOnly); + Future getAlbumByName( + String albumName, { + bool? remote, + bool? shared, + bool? owner, + }) => + _albumService.getAlbumByName( + albumName, + remote: remote, + shared: shared, + owner: owner, + ); /// Create an album on the server with the same name as the selected album for backup /// First this will check if the album already exists on the server with name @@ -55,7 +65,7 @@ class AlbumNotifier extends StateNotifier> { Future createSyncAlbum( String albumName, ) async { - final album = await getAlbumByName(albumName, remoteOnly: true); + final album = await getAlbumByName(albumName, remote: true, owner: true); if (album != null) { return; } diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart new file mode 100644 index 0000000000..a5a42ec796 --- /dev/null +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -0,0 +1,143 @@ +import 'dart:io'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/share_intent_service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; + +final shareIntentUploadProvider = StateNotifierProvider< + ShareIntentUploadStateNotifier, List>( + ((ref) => ShareIntentUploadStateNotifier( + ref.watch(appRouterProvider), + ref.watch(uploadServiceProvider), + ref.watch(shareIntentServiceProvider), + )), +); + +class ShareIntentUploadStateNotifier + extends StateNotifier> { + final AppRouter router; + final UploadService _uploadService; + final ShareIntentService _shareIntentService; + + ShareIntentUploadStateNotifier( + this.router, + this._uploadService, + this._shareIntentService, + ) : super([]) { + _uploadService.onUploadStatus = _uploadStatusCallback; + _uploadService.onTaskProgress = _taskProgressCallback; + } + + void init() { + _shareIntentService.onSharedMedia = onSharedMedia; + _shareIntentService.init(); + } + + void onSharedMedia(List attachments) { + router.removeWhere((route) => route.name == "ShareIntentRoute"); + clearAttachments(); + addAttachments(attachments); + router.push(ShareIntentRoute(attachments: attachments)); + } + + void addAttachments(List attachments) { + if (attachments.isEmpty) { + return; + } + state = [...state, ...attachments]; + } + + void removeAttachment(ShareIntentAttachment attachment) { + final updatedState = + state.where((element) => element != attachment).toList(); + if (updatedState.length != state.length) { + state = updatedState; + } + } + + void clearAttachments() { + if (state.isEmpty) { + return; + } + + state = []; + } + + void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async { + if (status == TaskStatus.canceled) { + return; + } + + final taskId = task.task.taskId; + final uploadStatus = switch (task.status) { + TaskStatus.complete => UploadStatus.complete, + TaskStatus.failed => UploadStatus.failed, + TaskStatus.canceled => UploadStatus.canceled, + TaskStatus.enqueued => UploadStatus.enqueued, + TaskStatus.running => UploadStatus.running, + TaskStatus.paused => UploadStatus.paused, + TaskStatus.notFound => UploadStatus.notFound, + TaskStatus.waitingToRetry => UploadStatus.waitingtoRetry + }; + + state = [ + for (final attachment in state) + if (attachment.id == taskId.toInt()) + attachment.copyWith(status: uploadStatus) + else + attachment, + ]; + } + + void _uploadStatusCallback(TaskStatusUpdate update) { + _updateUploadStatus(update, update.status); + + switch (update.status) { + case TaskStatus.complete: + if (update.responseStatusCode == 200) { + if (kDebugMode) { + debugPrint("[COMPLETE] ${update.task.taskId} - DUPLICATE"); + } + } else { + if (kDebugMode) { + debugPrint("[COMPLETE] ${update.task.taskId}"); + } + } + break; + + default: + break; + } + } + + void _taskProgressCallback(TaskProgressUpdate update) { + // Ignore if the task is cancled or completed + if (update.progress == downloadFailed || + update.progress == downloadCompleted) { + return; + } + + final taskId = update.task.taskId; + state = [ + for (final attachment in state) + if (attachment.id == taskId.toInt()) + attachment.copyWith(uploadProgress: update.progress) + else + attachment, + ]; + } + + Future upload(File file) { + return _uploadService.upload(file); + } + + Future cancelUpload(String id) { + return _uploadService.cancelUpload(id); + } +} diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart index 2c78e4c238..adf83b33d4 100644 --- a/mobile/lib/repositories/album.repository.dart +++ b/mobile/lib/repositories/album.repository.dart @@ -34,11 +34,25 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { Future create(Album album) => txn(() => db.albums.store(album)); @override - Future getByName(String name, {bool? shared, bool? remote}) { + Future getByName( + String name, { + bool? shared, + bool? remote, + bool? owner, + }) { var query = db.albums.filter().nameEqualTo(name); if (shared != null) { query = query.sharedEqualTo(shared); } + if (owner == true) { + query = query.owner( + (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + ); + } else if (owner == false) { + query = query.owner( + (q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + ); + } if (remote == true) { query = query.localIdIsNull(); } else if (remote == false) { diff --git a/mobile/lib/repositories/share_handler.repository.dart b/mobile/lib/repositories/share_handler.repository.dart new file mode 100644 index 0000000000..4c07b662a8 --- /dev/null +++ b/mobile/lib/repositories/share_handler.repository.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/share_handler.interface.dart'; +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; +import 'package:share_handler/share_handler.dart'; + +final shareHandlerRepositoryProvider = Provider( + (ref) => ShareHandlerRepository(), +); + +class ShareHandlerRepository implements IShareHandlerRepository { + ShareHandlerRepository(); + + @override + void Function(List attachments)? onSharedMedia; + + @override + Future init() async { + final handler = ShareHandlerPlatform.instance; + final media = await handler.getInitialSharedMedia(); + + if (media != null && media.attachments != null) { + onSharedMedia?.call(_buildPayload(media.attachments!)); + } + + handler.sharedMediaStream.listen((SharedMedia media) { + if (media.attachments != null) { + onSharedMedia?.call(_buildPayload(media.attachments!)); + } + }); + } + + List _buildPayload( + List attachments, + ) { + final payload = []; + + for (final attachment in attachments) { + if (attachment == null) { + continue; + } + + final type = attachment.type == SharedAttachmentType.image + ? ShareIntentAttachmentType.image + : ShareIntentAttachmentType.video; + + final fileLength = File(attachment.path).lengthSync(); + + payload.add( + ShareIntentAttachment( + path: attachment.path, + type: type, + status: UploadStatus.enqueued, + uploadProgress: 0.0, + fileLength: fileLength, + ), + ); + } + + return payload; + } +} diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart new file mode 100644 index 0000000000..6445d144f6 --- /dev/null +++ b/mobile/lib/repositories/upload.repository.dart @@ -0,0 +1,42 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/upload.interface.dart'; +import 'package:immich_mobile/utils/upload.dart'; + +final uploadRepositoryProvider = Provider((ref) => UploadRepository()); + +class UploadRepository implements IUploadRepository { + @override + void Function(TaskStatusUpdate)? onUploadStatus; + + @override + void Function(TaskProgressUpdate)? onTaskProgress; + + UploadRepository() { + FileDownloader().registerCallbacks( + group: uploadGroup, + taskStatusCallback: (update) => onUploadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + } + + @override + Future upload(UploadTask task) { + return FileDownloader().enqueue(task); + } + + @override + Future deleteAllTrackingRecords() { + return FileDownloader().database.deleteAllRecords(); + } + + @override + Future cancel(String id) { + return FileDownloader().cancelTaskWithId(id); + } + + @override + Future deleteRecordsWithIds(List ids) { + return FileDownloader().database.deleteRecordsWithIds(ids); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 5adfeb4061..3078a0dc1a 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/backup/album_preview.page.dart'; import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; @@ -57,6 +58,7 @@ import 'package:immich_mobile/pages/library/partner/partner.page.dart'; import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; +import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -277,6 +279,10 @@ class AppRouter extends RootStackRouter { page: NativeVideoViewerRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: ShareIntentRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 3bd8966175..48528fdfe2 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -136,15 +136,10 @@ class AlbumAssetSelectionRouteArgs { /// generated route for /// [AlbumOptionsPage] -class AlbumOptionsRoute extends PageRouteInfo { - AlbumOptionsRoute({ - Key? key, - List? children, - }) : super( +class AlbumOptionsRoute extends PageRouteInfo { + const AlbumOptionsRoute({List? children}) + : super( AlbumOptionsRoute.name, - args: AlbumOptionsRouteArgs( - key: key, - ), initialChildren: children, ); @@ -153,25 +148,11 @@ class AlbumOptionsRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = data.argsAs(); - return AlbumOptionsPage( - key: args.key, - ); + return const AlbumOptionsPage(); }, ); } -class AlbumOptionsRouteArgs { - const AlbumOptionsRouteArgs({this.key}); - - final Key? key; - - @override - String toString() { - return 'AlbumOptionsRouteArgs{key: $key}'; - } -} - /// generated route for /// [AlbumPreviewPage] class AlbumPreviewRoute extends PageRouteInfo { @@ -1453,6 +1434,52 @@ class SettingsSubRouteArgs { } } +/// generated route for +/// [ShareIntentPage] +class ShareIntentRoute extends PageRouteInfo { + ShareIntentRoute({ + Key? key, + required List attachments, + List? children, + }) : super( + ShareIntentRoute.name, + args: ShareIntentRouteArgs( + key: key, + attachments: attachments, + ), + initialChildren: children, + ); + + static const String name = 'ShareIntentRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return ShareIntentPage( + key: args.key, + attachments: args.attachments, + ); + }, + ); +} + +class ShareIntentRouteArgs { + const ShareIntentRouteArgs({ + this.key, + required this.attachments, + }); + + final Key? key; + + final List attachments; + + @override + String toString() { + return 'ShareIntentRouteArgs{key: $key, attachments: $attachments}'; + } +} + /// generated route for /// [SharedLinkEditPage] class SharedLinkEditRoute extends PageRouteInfo { diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 5f013c0e53..a993705e11 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -170,7 +170,12 @@ class AlbumService { try { await _userService.refreshUsers(); final (sharedAlbum, ownedAlbum) = await ( + // Note: `shared: true` is required to get albums that don't belong to + // us due to unusual behaviour on the API but this will also return our + // own shared albums _albumApiRepository.getAll(shared: true), + // Passing null (or nothing) for `shared` returns only albums that + // explicitly belong to us _albumApiRepository.getAll(shared: null) ).wait; @@ -212,7 +217,7 @@ class AlbumService { for (int round = 0;; round++) { final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - if (null == await _albumRepository.getByName(proposedName)) { + if (null == await _albumRepository.getByName(proposedName, owner: true)) { return proposedName; } } @@ -408,8 +413,18 @@ class AlbumService { } } - Future getAlbumByName(String name, bool remoteOnly) => - _albumRepository.getByName(name, remote: remoteOnly ? true : null); + Future getAlbumByName( + String name, { + bool? remote, + bool? shared, + bool? owner, + }) => + _albumRepository.getByName( + name, + remote: remote, + shared: shared, + owner: owner, + ); /// /// Add the uploaded asset to the selected albums @@ -419,7 +434,7 @@ class AlbumService { List assetIds, ) async { for (final albumName in albumNames) { - Album? album = await getAlbumByName(albumName, true); + Album? album = await getAlbumByName(albumName, remote: true, owner: true); album ??= await createAlbum(albumName, []); if (album != null && album.remoteId != null) { await _albumApiRepository.addAssets(album.remoteId!, assetIds); diff --git a/mobile/lib/services/share_intent_service.dart b/mobile/lib/services/share_intent_service.dart new file mode 100644 index 0000000000..e514e5bbdc --- /dev/null +++ b/mobile/lib/services/share_intent_service.dart @@ -0,0 +1,23 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; +import 'package:immich_mobile/repositories/share_handler.repository.dart'; + +final shareIntentServiceProvider = Provider( + (ref) => ShareIntentService( + ref.watch(shareHandlerRepositoryProvider), + ), +); + +class ShareIntentService { + final ShareHandlerRepository shareHandlerRepository; + void Function(List attachments)? onSharedMedia; + + ShareIntentService( + this.shareHandlerRepository, + ); + + void init() { + shareHandlerRepository.onSharedMedia = onSharedMedia; + shareHandlerRepository.init(); + } +} diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart new file mode 100644 index 0000000000..1ffe01bb93 --- /dev/null +++ b/mobile/lib/services/upload.service.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/interfaces/upload.interface.dart'; +import 'package:immich_mobile/repositories/upload.repository.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/upload.dart'; +import 'package:path/path.dart'; +// import 'package:logging/logging.dart'; + +final uploadServiceProvider = Provider( + (ref) => UploadService( + ref.watch(uploadRepositoryProvider), + ), +); + +class UploadService { + final IUploadRepository _uploadRepository; + // final Logger _log = Logger("UploadService"); + void Function(TaskStatusUpdate)? onUploadStatus; + void Function(TaskProgressUpdate)? onTaskProgress; + + UploadService( + this._uploadRepository, + ) { + _uploadRepository.onUploadStatus = _onUploadCallback; + _uploadRepository.onTaskProgress = _onTaskProgressCallback; + } + + void _onTaskProgressCallback(TaskProgressUpdate update) { + onTaskProgress?.call(update); + } + + void _onUploadCallback(TaskStatusUpdate update) { + onUploadStatus?.call(update); + } + + Future cancelUpload(String id) { + return FileDownloader().cancelTaskWithId(id); + } + + Future upload(File file) async { + final task = await _buildUploadTask( + hash(file.path).toString(), + file, + ); + + await _uploadRepository.upload(task); + } + + Future _buildUploadTask( + String id, + File file, { + Map? fields, + }) async { + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final url = Uri.parse('$serverEndpoint/assets').toString(); + final headers = ApiService.getRequestHeaders(); + final deviceId = Store.get(StoreKey.deviceId); + + final (baseDirectory, directory, filename) = + await Task.split(filePath: file.path); + final stats = await file.stat(); + final fileCreatedAt = stats.changed; + final fileModifiedAt = stats.modified; + + final fieldsMap = { + 'filename': filename, + 'deviceAssetId': id, + 'deviceId': deviceId, + 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), + 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), + 'isFavorite': 'false', + 'duration': '0', + if (fields != null) ...fields, + }; + + return UploadTask( + taskId: id, + httpRequestMethod: 'POST', + url: url, + headers: headers, + filename: filename, + fields: fieldsMap, + baseDirectory: baseDirectory, + directory: directory, + fileField: 'assetData', + group: uploadGroup, + updates: Updates.statusAndProgress, + ); + } +} diff --git a/mobile/lib/utils/bytes_units.dart b/mobile/lib/utils/bytes_units.dart index ea9d0f5cf5..3a73e5b320 100644 --- a/mobile/lib/utils/bytes_units.dart +++ b/mobile/lib/utils/bytes_units.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + String formatBytes(int bytes) { const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; @@ -14,3 +16,10 @@ String formatBytes(int bytes) { return "${remainder.toStringAsFixed(magnitude == 0 ? 0 : 1)} ${units[magnitude]}"; } + +String formatHumanReadableBytes(int bytes, int decimals) { + if (bytes <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB"]; + var i = (log(bytes) / log(1024)).floor(); + return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}'; +} diff --git a/mobile/lib/utils/upload.dart b/mobile/lib/utils/upload.dart new file mode 100644 index 0000000000..a0b77f1d93 --- /dev/null +++ b/mobile/lib/utils/upload.dart @@ -0,0 +1 @@ +const uploadGroup = 'upload_group'; diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 7c36ebc21d..b058f29e7d 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -206,7 +206,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ), ListTile( leading: const Icon(Icons.settings_rounded), - onTap: () => context.navigateTo(AlbumOptionsRoute()), + onTap: () => context.navigateTo(const AlbumOptionsRoute()), title: const Text( "translated_text_options", style: TextStyle(fontWeight: FontWeight.w500), diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 80b691baa0..5a15bf5f5e 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1328,6 +1328,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + share_handler: + dependency: "direct main" + description: + name: share_handler + sha256: "76575533be04df3fecbebd3c5b5325a8271b5973131f8b8b0ab8490c395a5d37" + url: "https://pub.dev" + source: hosted + version: "0.0.22" + share_handler_android: + dependency: transitive + description: + name: share_handler_android + sha256: "124dcc914fb7ecd89076d3dc28435b98fe2129a988bf7742f7a01dcb66a95667" + url: "https://pub.dev" + source: hosted + version: "0.0.9" + share_handler_ios: + dependency: transitive + description: + name: share_handler_ios + sha256: cdc21f88f336a944157a8e9ceb191525cee3b082d6eb6c2082488e4f09dc3ece + url: "https://pub.dev" + source: hosted + version: "0.0.15" + share_handler_platform_interface: + dependency: transitive + description: + name: share_handler_platform_interface + sha256: "7a4df95a87b326b2f07458d937f2281874567c364b7b7ebe4e7d50efaae5f106" + url: "https://pub.dev" + source: hosted + version: "0.0.6" share_plus: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 12433e0bfc..40b4345992 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -77,6 +77,7 @@ dependencies: image_picker: ^1.0.7 # only used to select user profile image from system gallery -> we can simply select an image from within immich? logging: ^1.2.0 file_picker: ^8.0.0+1 + share_handler: ^0.0.22 # This is uncommented in F-Droid build script # Taken from https://github.com/Myzel394/locus/blob/445013d22ec1d759027d4303bd65b30c5c8588c8/pubspec.yaml#L105 diff --git a/renovate.json b/renovate.json index dd3ca1ad59..2634eaef4d 100644 --- a/renovate.json +++ b/renovate.json @@ -6,6 +6,10 @@ ], "minimumReleaseAge": "5 days", "packageRules": [ + { + "groupName": "@immich/ui", + "matchPackageNames": ["@immich/ui"] + }, { "matchFileNames": [ "cli/**", diff --git a/server/.prettierignore b/server/.prettierignore index 27a20b3f50..8dbb1067a9 100644 --- a/server/.prettierignore +++ b/server/.prettierignore @@ -10,6 +10,7 @@ node_modules coverage dist **/migrations/** +db.d.ts # Ignore files for PNPM, NPM and YARN pnpm-lock.yaml diff --git a/server/Dockerfile b/server/Dockerfile index 85c3ffae1f..84902df3ca 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20250107@sha256:d00ab37e1c1ed87b799d6509fbc825a721ca0723c59c67955217826882017d38 AS dev +FROM ghcr.io/immich-app/base-server-dev:20250114@sha256:fce0404484bde5afc38a4399c6b25895eb079a666d269f199c93dfbfdd5b26b6 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS web +FROM node:22.13.0-alpine3.20@sha256:db8dcb90326a0116375414e9a7c068a6b87a4422b7da37b5c6cd026f7c7835d3 AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ @@ -42,7 +42,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20250107@sha256:78e92f113103271d43a3b050370b21b31c3c14792d3d23b18b542581a440c72b +FROM ghcr.io/immich-app/base-server-prod:20250114@sha256:94ec8a36cdf11691810c4aeccee1b49b00348e17f6b6781d87dd48a74e6c6787 WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/package.json b/server/package.json index 14b4e2fa32..be6ff2632f 100644 --- a/server/package.json +++ b/server/package.json @@ -29,6 +29,7 @@ "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js", "typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'", "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run", + "kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts", "sync:open-api": "node ./dist/bin/sync-open-api.js", "sync:sql": "node ./dist/bin/sync-sql.js", "email:dev": "email dev -p 3050 --dir src/emails" diff --git a/server/src/app.module.ts b/server/src/app.module.ts index d0422756b6..5e6432408f 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -20,14 +20,14 @@ import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; -import { repositories } from 'src/repositories'; +import { providers, repositories } from 'src/repositories'; import { ConfigRepository } from 'src/repositories/config.repository'; import { teardownTelemetry } from 'src/repositories/telemetry.repository'; import { services } from 'src/services'; import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; -const common = [...services, ...repositories]; +const common = [...services, ...providers, ...repositories]; const middleware = [ FileUploadInterceptor, @@ -73,7 +73,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { } async onModuleInit() { - this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) }); + this.telemetryRepository.setup({ repositories: [...providers.map(({ useClass }) => useClass), ...repositories] }); this.jobRepository.setup({ services }); if (this.worker === ImmichWorker.MICROSERVICES) { diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 2de4fb4127..21eff3b306 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -12,7 +12,7 @@ import { format } from 'sql-formatter'; import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators'; import { entities } from 'src/entities'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { repositories } from 'src/repositories'; +import { providers, repositories } from 'src/repositories'; import { AccessRepository } from 'src/repositories/access.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { AuthService } from 'src/services/auth.service'; @@ -43,7 +43,7 @@ export class SqlLogger implements Logger { const reflector = new Reflector(); -type Repository = (typeof repositories)[0]['useClass']; +type Repository = (typeof providers)[0]['useClass']; type Provider = { provide: any; useClass: Repository }; type SqlGeneratorOptions = { targetDir: string }; @@ -57,7 +57,11 @@ class SqlGenerator { async run() { try { await this.setup(); - for (const repository of repositories) { + const targets = [ + ...providers, + ...repositories.map((repository) => ({ provide: repository, useClass: repository as any })), + ]; + for (const repository of targets) { if (repository.provide === ILoggerRepository) { continue; } @@ -86,6 +90,7 @@ class SqlGenerator { this.sqlLogger.logQuery(event.query.sql); } else if (event.level === 'error') { this.sqlLogger.logQueryError(event.error as Error, event.query.sql); + this.sqlLogger.logQuery(event.query.sql); } }, }), @@ -98,7 +103,7 @@ class SqlGenerator { TypeOrmModule.forFeature(entities), OpenTelemetryModule.forRoot(otel), ], - providers: [...repositories, AuthService, SchedulerRegistry], + providers: [...providers, ...repositories, AuthService, SchedulerRegistry], }).compile(); this.app = await moduleFixture.createNestApplication().init(); diff --git a/server/src/database.ts b/server/src/database.ts new file mode 100644 index 0000000000..fce9ede561 --- /dev/null +++ b/server/src/database.ts @@ -0,0 +1,3 @@ +export const columns = { + userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'], +} as const; diff --git a/server/src/db.d.ts b/server/src/db.d.ts index a5cab5dab7..6242914bee 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -3,16 +3,21 @@ * Please do not edit it manually. */ -import type { ColumnType } from 'kysely'; +import type { ColumnType } from "kysely"; -export type ArrayType = ArrayTypeImpl extends (infer U)[] ? U[] : ArrayTypeImpl; +export type ArrayType = ArrayTypeImpl extends (infer U)[] + ? U[] + : ArrayTypeImpl; -export type ArrayTypeImpl = T extends ColumnType ? ColumnType : T[]; +export type ArrayTypeImpl = T extends ColumnType + ? ColumnType + : T[]; -export type AssetsStatusEnum = 'active' | 'deleted' | 'trashed'; +export type AssetsStatusEnum = "active" | "deleted" | "trashed"; -export type Generated = - T extends ColumnType ? ColumnType : ColumnType; +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; export type Int8 = ColumnType; @@ -28,7 +33,7 @@ export type JsonPrimitive = boolean | number | string | null; export type JsonValue = JsonArray | JsonObject | JsonPrimitive; -export type Sourcetype = 'exif' | 'machine-learning'; +export type Sourcetype = "exif" | "machine-learning"; export type Timestamp = ColumnType; @@ -257,7 +262,7 @@ export interface NaturalearthCountries { admin: string; admin_a3: string; coordinates: string; - id: number; + id: Generated; type: string; } @@ -433,6 +438,6 @@ export interface DB { tags_closure: TagsClosure; user_metadata: UserMetadata; users: Users; - 'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat; + "vectors.pg_vector_index_stat": VectorsPgVectorIndexStat; version_history: VersionHistory; } diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 4bc0065244..9a0307f46b 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { ActivityEntity } from 'src/entities/activity.entity'; +import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; +import { UserEntity } from 'src/entities/user.entity'; +import { ActivityItem } from 'src/types'; import { Optional, ValidateUUID } from 'src/validation'; export enum ReactionType { @@ -67,13 +68,13 @@ export class ActivityCreateDto extends ActivityDto { comment?: string; } -export function mapActivity(activity: ActivityEntity): ActivityResponseDto { +export const mapActivity = (activity: ActivityItem): ActivityResponseDto => { return { id: activity.id, assetId: activity.assetId, createdAt: activity.createdAt, comment: activity.comment, type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT, - user: mapUser(activity.user), + user: mapUser(activity.user as unknown as UserEntity), }; -} +}; diff --git a/server/src/interfaces/activity.interface.ts b/server/src/interfaces/activity.interface.ts deleted file mode 100644 index c42d3cc8aa..0000000000 --- a/server/src/interfaces/activity.interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Insertable } from 'kysely'; -import { Activity } from 'src/db'; -import { ActivityEntity } from 'src/entities/activity.entity'; -import { ActivitySearch } from 'src/repositories/activity.repository'; - -export const IActivityRepository = 'IActivityRepository'; - -export interface IActivityRepository { - search(options: ActivitySearch): Promise; - create(activity: Insertable): Promise; - delete(id: string): Promise; - getStatistics(options: { albumId: string; assetId?: string }): Promise; -} diff --git a/server/src/interfaces/album-user.interface.ts b/server/src/interfaces/album-user.interface.ts index d5742ad788..835e835589 100644 --- a/server/src/interfaces/album-user.interface.ts +++ b/server/src/interfaces/album-user.interface.ts @@ -1,14 +1,18 @@ -import { AlbumUserEntity } from 'src/entities/album-user.entity'; +import { Insertable, Selectable, Updateable } from 'kysely'; +import { AlbumsSharedUsersUsers } from 'src/db'; export const IAlbumUserRepository = 'IAlbumUserRepository'; export type AlbumPermissionId = { - albumId: string; - userId: string; + albumsId: string; + usersId: string; }; export interface IAlbumUserRepository { - create(albumUser: Partial): Promise; - update({ userId, albumId }: AlbumPermissionId, albumPermission: Partial): Promise; - delete({ userId, albumId }: AlbumPermissionId): Promise; + create(albumUser: Insertable): Promise>; + update( + id: AlbumPermissionId, + albumPermission: Updateable, + ): Promise>; + delete(id: AlbumPermissionId): Promise; } diff --git a/server/src/interfaces/library.interface.ts b/server/src/interfaces/library.interface.ts index d8f1a13031..66e9a7de29 100644 --- a/server/src/interfaces/library.interface.ts +++ b/server/src/interfaces/library.interface.ts @@ -1,3 +1,5 @@ +import { Insertable, Updateable } from 'kysely'; +import { Libraries } from 'src/db'; import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; import { LibraryEntity } from 'src/entities/library.entity'; @@ -6,10 +8,10 @@ export const ILibraryRepository = 'ILibraryRepository'; export interface ILibraryRepository { getAll(withDeleted?: boolean): Promise; getAllDeleted(): Promise; - get(id: string, withDeleted?: boolean): Promise; - create(library: Partial): Promise; + get(id: string, withDeleted?: boolean): Promise; + create(library: Insertable): Promise; delete(id: string): Promise; softDelete(id: string): Promise; - update(library: Partial): Promise; + update(id: string, library: Updateable): Promise; getStatistics(id: string): Promise; } diff --git a/server/src/interfaces/memory.interface.ts b/server/src/interfaces/memory.interface.ts index 308943d97e..b1dbcbef85 100644 --- a/server/src/interfaces/memory.interface.ts +++ b/server/src/interfaces/memory.interface.ts @@ -1,4 +1,6 @@ -import { MemoryEntity } from 'src/entities/memory.entity'; +import { Insertable, Updateable } from 'kysely'; +import { Memories } from 'src/db'; +import { MemoryEntity, OnThisDayData } from 'src/entities/memory.entity'; import { IBulkAsset } from 'src/utils/asset.util'; export const IMemoryRepository = 'IMemoryRepository'; @@ -6,7 +8,10 @@ export const IMemoryRepository = 'IMemoryRepository'; export interface IMemoryRepository extends IBulkAsset { search(ownerId: string): Promise; get(id: string): Promise; - create(memory: Partial): Promise; - update(memory: Partial): Promise; + create( + memory: Omit, 'data'> & { data: OnThisDayData }, + assetIds: Set, + ): Promise; + update(id: string, memory: Updateable): Promise; delete(id: string): Promise; } diff --git a/server/src/interfaces/partner.interface.ts b/server/src/interfaces/partner.interface.ts index 842745a51e..a6f50178ca 100644 --- a/server/src/interfaces/partner.interface.ts +++ b/server/src/interfaces/partner.interface.ts @@ -1,3 +1,5 @@ +import { Updateable } from 'kysely'; +import { Partners } from 'src/db'; import { PartnerEntity } from 'src/entities/partner.entity'; export interface PartnerIds { @@ -14,8 +16,8 @@ export const IPartnerRepository = 'IPartnerRepository'; export interface IPartnerRepository { getAll(userId: string): Promise; - get(partner: PartnerIds): Promise; + get(partner: PartnerIds): Promise; create(partner: PartnerIds): Promise; - remove(entity: PartnerEntity): Promise; - update(entity: Partial): Promise; + remove(partner: PartnerIds): Promise; + update(partner: PartnerIds, entity: Updateable): Promise; } diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index ad57eac0ad..db69ac670e 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -1,180 +1,137 @@ -- NOTE: This file is auto generated by ./sql-generator -- AccessRepository.activity.checkOwnerAccess -SELECT - "ActivityEntity"."id" AS "ActivityEntity_id" -FROM - "activity" "ActivityEntity" -WHERE - ( - ("ActivityEntity"."id" IN ($1)) - AND ("ActivityEntity"."userId" = $2) - ) +select + "activity"."id" +from + "activity" +where + "activity"."id" in ($1) + and "activity"."userId" = $2 -- AccessRepository.activity.checkAlbumOwnerAccess -SELECT - "ActivityEntity"."id" AS "ActivityEntity_id" -FROM - "activity" "ActivityEntity" - LEFT JOIN "albums" "ActivityEntity__ActivityEntity_album" ON "ActivityEntity__ActivityEntity_album"."id" = "ActivityEntity"."albumId" - AND ( - "ActivityEntity__ActivityEntity_album"."deletedAt" IS NULL - ) -WHERE - ( - ("ActivityEntity"."id" IN ($1)) - AND ( - ( - ( - "ActivityEntity__ActivityEntity_album"."ownerId" = $2 - ) - ) - ) - ) +select + "activity"."id" +from + "activity" + left join "albums" on "activity"."albumId" = "albums"."id" + and "albums"."deletedAt" is null +where + "activity"."id" in ($1) + and "albums"."ownerId" = $2::uuid -- AccessRepository.activity.checkCreateAccess -SELECT - "album"."id" AS "album_id" -FROM - "albums" "album" - LEFT JOIN "albums_shared_users_users" "album_albumUsers_users" ON "album_albumUsers_users"."albumsId" = "album"."id" - LEFT JOIN "users" "albumUsers" ON "albumUsers"."id" = "album_albumUsers_users"."usersId" - AND ("albumUsers"."deletedAt" IS NULL) -WHERE - ( - "album"."id" IN ($1) - AND "album"."isActivityEnabled" = true - AND ( - "album"."ownerId" = $2 - OR "albumUsers"."id" = $2 - ) +select + "albums"."id" +from + "albums" + left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id" + left join "users" on "users"."id" = "albumUsers"."usersId" + and "users"."deletedAt" is null +where + "albums"."id" in ($1) + and "albums"."isActivityEnabled" = $2 + and ( + "albums"."ownerId" = $3 + or "users"."id" = $4 ) - AND ("album"."deletedAt" IS NULL) + and "albums"."deletedAt" is null -- AccessRepository.album.checkOwnerAccess -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id" -FROM - "albums" "AlbumEntity" -WHERE - ( - ( - ("AlbumEntity"."id" IN ($1)) - AND ("AlbumEntity"."ownerId" = $2) - ) - ) - AND ("AlbumEntity"."deletedAt" IS NULL) +select + "albums"."id" +from + "albums" +where + "albums"."id" in ($1) + and "albums"."ownerId" = $2 + and "albums"."deletedAt" is null -- AccessRepository.album.checkSharedAlbumAccess -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id" -FROM - "albums" "AlbumEntity" - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" - AND ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL - ) -WHERE - ( - ( - ("AlbumEntity"."id" IN ($1)) - AND ( - ( - ( - ( - ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = $2 - ) - ) - ) - AND ( - "AlbumEntity__AlbumEntity_albumUsers"."role" IN ($3, $4) - ) - ) - ) - ) - ) - AND ("AlbumEntity"."deletedAt" IS NULL) +select + "albums"."id" +from + "albums" + left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id" + left join "users" on "users"."id" = "albumUsers"."usersId" + and "users"."deletedAt" is null +where + "albums"."id" in ($1) + and "albums"."deletedAt" is null + and "users"."id" = $2 + and "albumUsers"."role" in ($3, $4) -- AccessRepository.album.checkSharedLinkAccess -SELECT - "SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId", - "SharedLinkEntity"."id" AS "SharedLinkEntity_id" -FROM - "shared_links" "SharedLinkEntity" -WHERE - ( - ("SharedLinkEntity"."id" = $1) - AND ("SharedLinkEntity"."albumId" IN ($2)) - ) +select + "shared_links"."albumId" +from + "shared_links" +where + "shared_links"."id" = $1 + and "shared_links"."albumId" in ($2) -- AccessRepository.asset.checkAlbumAccess -SELECT - "asset"."id" AS "assetId", - "asset"."livePhotoVideoId" AS "livePhotoVideoId" -FROM - "albums" "album" - INNER JOIN "albums_assets_assets" "album_asset" ON "album_asset"."albumsId" = "album"."id" - INNER JOIN "assets" "asset" ON "asset"."id" = "album_asset"."assetsId" - AND ("asset"."deletedAt" IS NULL) - LEFT JOIN "albums_shared_users_users" "album_albumUsers_users" ON "album_albumUsers_users"."albumsId" = "album"."id" - LEFT JOIN "users" "albumUsers" ON "albumUsers"."id" = "album_albumUsers_users"."usersId" - AND ("albumUsers"."deletedAt" IS NULL) -WHERE - ( - array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid [] - AND ( - "album"."ownerId" = $2 - OR "albumUsers"."id" = $2 - ) +select + "assets"."id", + "assets"."livePhotoVideoId" +from + "albums" + inner join "albums_assets_assets" as "albumAssets" on "albums"."id" = "albumAssets"."albumsId" + inner join "assets" on "assets"."id" = "albumAssets"."assetsId" + and "assets"."deletedAt" is null + left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id" + left join "users" on "users"."id" = "albumUsers"."usersId" + and "users"."deletedAt" is null +where + array["assets"."id", "assets"."livePhotoVideoId"] && array[$1]::uuid [] + and ( + "albums"."ownerId" = $2 + or "users"."id" = $3 ) - AND ("album"."deletedAt" IS NULL) + and "albums"."deletedAt" is null -- AccessRepository.asset.checkOwnerAccess -SELECT - "AssetEntity"."id" AS "AssetEntity_id" -FROM - "assets" "AssetEntity" -WHERE - ( - ("AssetEntity"."id" IN ($1)) - AND ("AssetEntity"."ownerId" = $2) - ) +select + "assets"."id" +from + "assets" +where + "assets"."id" in ($1) + and "assets"."ownerId" = $2 -- AccessRepository.asset.checkPartnerAccess -SELECT - "asset"."id" AS "assetId" -FROM - "partners" "partner" - INNER JOIN "users" "sharedBy" ON "sharedBy"."id" = "partner"."sharedById" - AND ("sharedBy"."deletedAt" IS NULL) - INNER JOIN "assets" "asset" ON "asset"."ownerId" = "sharedBy"."id" - AND ("asset"."deletedAt" IS NULL) -WHERE +select + "assets"."id" +from + "partners" as "partner" + inner join "users" as "sharedBy" on "sharedBy"."id" = "partner"."sharedById" + and "sharedBy"."deletedAt" is null + inner join "assets" on "assets"."ownerId" = "sharedBy"."id" + and "assets"."deletedAt" is null +where "partner"."sharedWithId" = $1 - AND "asset"."isArchived" = false - AND "asset"."id" IN ($2) + and "assets"."isArchived" = $2 + and "assets"."id" in ($3) -- AccessRepository.asset.checkSharedLinkAccess -SELECT - "assets"."id" AS "assetId", - "assets"."livePhotoVideoId" AS "assetLivePhotoVideoId", - "albumAssets"."id" AS "albumAssetId", - "albumAssets"."livePhotoVideoId" AS "albumAssetLivePhotoVideoId" -FROM - "shared_links" "sharedLink" - LEFT JOIN "albums" "album" ON "album"."id" = "sharedLink"."albumId" - AND ("album"."deletedAt" IS NULL) - LEFT JOIN "shared_link__asset" "assets_sharedLink" ON "assets_sharedLink"."sharedLinksId" = "sharedLink"."id" - LEFT JOIN "assets" "assets" ON "assets"."id" = "assets_sharedLink"."assetsId" - AND ("assets"."deletedAt" IS NULL) - LEFT JOIN "albums_assets_assets" "album_albumAssets" ON "album_albumAssets"."albumsId" = "album"."id" - LEFT JOIN "assets" "albumAssets" ON "albumAssets"."id" = "album_albumAssets"."assetsId" - AND ("albumAssets"."deletedAt" IS NULL) -WHERE - "sharedLink"."id" = $1 - AND array[ +select + "assets"."id" as "assetId", + "assets"."livePhotoVideoId" as "assetLivePhotoVideoId", + "albumAssets"."id" as "albumAssetId", + "albumAssets"."livePhotoVideoId" as "albumAssetLivePhotoVideoId" +from + "shared_links" + left join "albums" on "albums"."id" = "shared_links"."albumId" + and "albums"."deletedAt" is null + left join "shared_link__asset" on "shared_link__asset"."sharedLinksId" = "shared_links"."id" + left join "assets" on "assets"."id" = "shared_link__asset"."assetsId" + and "assets"."deletedAt" is null + left join "albums_assets_assets" on "albums_assets_assets"."albumsId" = "albums"."id" + left join "assets" as "albumAssets" on "albumAssets"."id" = "albums_assets_assets"."assetsId" + and "albumAssets"."deletedAt" is null +where + "shared_links"."id" = $1 + and array[ "assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", @@ -182,100 +139,76 @@ WHERE ] && array[$2]::uuid [] -- AccessRepository.authDevice.checkOwnerAccess -SELECT - "SessionEntity"."id" AS "SessionEntity_id" -FROM - "sessions" "SessionEntity" -WHERE - ( - ("SessionEntity"."userId" = $1) - AND ("SessionEntity"."id" IN ($2)) - ) +select + "sessions"."id" +from + "sessions" +where + "sessions"."userId" = $1 + and "sessions"."id" in ($2) -- AccessRepository.memory.checkOwnerAccess -SELECT - "MemoryEntity"."id" AS "MemoryEntity_id" -FROM - "memories" "MemoryEntity" -WHERE - ( - ( - ("MemoryEntity"."id" IN ($1)) - AND ("MemoryEntity"."ownerId" = $2) - ) - ) - AND ("MemoryEntity"."deletedAt" IS NULL) +select + "memories"."id" +from + "memories" +where + "memories"."id" in ($1) + and "memories"."ownerId" = $2 + and "memories"."deletedAt" is null -- AccessRepository.person.checkOwnerAccess -SELECT - "PersonEntity"."id" AS "PersonEntity_id" -FROM - "person" "PersonEntity" -WHERE - ( - ("PersonEntity"."id" IN ($1)) - AND ("PersonEntity"."ownerId" = $2) - ) +select + "person"."id" +from + "person" +where + "person"."id" in ($1) + and "person"."ownerId" = $2 -- AccessRepository.person.checkFaceOwnerAccess -SELECT - "AssetFaceEntity"."id" AS "AssetFaceEntity_id" -FROM - "asset_faces" "AssetFaceEntity" - LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId" - AND ( - "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL - ) -WHERE - ( - ("AssetFaceEntity"."id" IN ($1)) - AND ( - ( - ( - "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" = $2 - ) - ) - ) - ) +select + "asset_faces"."id" +from + "asset_faces" + left join "assets" on "assets"."id" = "asset_faces"."assetId" + and "assets"."deletedAt" is null +where + "asset_faces"."id" in ($1) + and "assets"."ownerId" = $2 -- AccessRepository.partner.checkUpdateAccess -SELECT - "partner"."sharedById" AS "partner_sharedById", - "partner"."sharedWithId" AS "partner_sharedWithId" -FROM - "partners" "partner" -WHERE - "partner"."sharedById" IN ($1) - AND "partner"."sharedWithId" = $2 +select + "partners"."sharedById" +from + "partners" +where + "partners"."sharedById" in ($1) + and "partners"."sharedWithId" = $2 -- AccessRepository.stack.checkOwnerAccess -SELECT - "StackEntity"."id" AS "StackEntity_id" -FROM - "asset_stack" "StackEntity" -WHERE - ( - ("StackEntity"."id" IN ($1)) - AND ("StackEntity"."ownerId" = $2) - ) +select + "stacks"."id" +from + "asset_stack" as "stacks" +where + "stacks"."id" in ($1) + and "stacks"."ownerId" = $2 -- AccessRepository.tag.checkOwnerAccess -SELECT - "TagEntity"."id" AS "TagEntity_id" -FROM - "tags" "TagEntity" -WHERE - ( - ("TagEntity"."id" IN ($1)) - AND ("TagEntity"."userId" = $2) - ) +select + "tags"."id" +from + "tags" +where + "tags"."id" in ($1) + and "tags"."userId" = $2 -- AccessRepository.timeline.checkPartnerAccess -SELECT - "partner"."sharedById" AS "partner_sharedById", - "partner"."sharedWithId" AS "partner_sharedWithId" -FROM - "partners" "partner" -WHERE - "partner"."sharedById" IN ($1) - AND "partner"."sharedWithId" = $2 +select + "partners"."sharedById" +from + "partners" +where + "partners"."sharedById" in ($1) + and "partners"."sharedWithId" = $2 diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 5f25a7dcbd..8e9bb11f25 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -9,7 +9,11 @@ select from ( select - * + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" from "users" where diff --git a/server/src/queries/album.user.repository.sql b/server/src/queries/album.user.repository.sql new file mode 100644 index 0000000000..d628e4980a --- /dev/null +++ b/server/src/queries/album.user.repository.sql @@ -0,0 +1,25 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AlbumUserRepository.create +insert into + "albums_shared_users_users" ("usersId", "albumsId") +values + ($1, $2) +returning + * + +-- AlbumUserRepository.update +update "albums_shared_users_users" +set + "role" = $1 +where + "usersId" = $2 + and "albumsId" = $3 +returning + * + +-- AlbumUserRepository.delete +delete from "albums_shared_users_users" +where + "usersId" = $1 + and "albumsId" = $2 diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index a5d6ba05db..347990f04c 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -1,150 +1,137 @@ -- NOTE: This file is auto generated by ./sql-generator -- LibraryRepository.get -SELECT DISTINCT - "distinctAlias"."LibraryEntity_id" AS "ids_LibraryEntity_id" -FROM +select + "libraries".*, ( - SELECT - "LibraryEntity"."id" AS "LibraryEntity_id", - "LibraryEntity"."name" AS "LibraryEntity_name", - "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", - "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", - "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", - "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", - "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", - "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", - "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", - "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", - "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", - "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", - "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", - "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", - "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", - "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", - "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", - "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", - "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", - "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", - "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" - FROM - "libraries" "LibraryEntity" - LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" - AND ( - "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL - ) - WHERE - ((("LibraryEntity"."id" = $1))) - AND ("LibraryEntity"."deletedAt" IS NULL) - ) "distinctAlias" -ORDER BY - "LibraryEntity_id" ASC -LIMIT - 1 + select + to_json(obj) + from + ( + select + "users"."id", + "users"."email", + "users"."createdAt", + "users"."profileImagePath", + "users"."isAdmin", + "users"."shouldChangePassword", + "users"."deletedAt", + "users"."oauthId", + "users"."updatedAt", + "users"."storageLabel", + "users"."name", + "users"."quotaSizeInBytes", + "users"."quotaUsageInBytes", + "users"."status", + "users"."profileChangedAt" + from + "users" + where + "users"."id" = "libraries"."ownerId" + ) as obj + ) as "owner" +from + "libraries" +where + "libraries"."id" = $1 + and "libraries"."deletedAt" is null -- LibraryRepository.getAll -SELECT - "LibraryEntity"."id" AS "LibraryEntity_id", - "LibraryEntity"."name" AS "LibraryEntity_name", - "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", - "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", - "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", - "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", - "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", - "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", - "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", - "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", - "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", - "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", - "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", - "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", - "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", - "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", - "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", - "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", - "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", - "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", - "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" -FROM - "libraries" "LibraryEntity" - LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" - AND ( - "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL - ) -WHERE - "LibraryEntity"."deletedAt" IS NULL -ORDER BY - "LibraryEntity"."createdAt" ASC +select + "libraries".*, + ( + select + to_json(obj) + from + ( + select + "users"."id", + "users"."email", + "users"."createdAt", + "users"."profileImagePath", + "users"."isAdmin", + "users"."shouldChangePassword", + "users"."deletedAt", + "users"."oauthId", + "users"."updatedAt", + "users"."storageLabel", + "users"."name", + "users"."quotaSizeInBytes", + "users"."quotaUsageInBytes", + "users"."status", + "users"."profileChangedAt" + from + "users" + where + "users"."id" = "libraries"."ownerId" + ) as obj + ) as "owner" +from + "libraries" +where + "libraries"."deletedAt" is null +order by + "createdAt" asc -- LibraryRepository.getAllDeleted -SELECT - "LibraryEntity"."id" AS "LibraryEntity_id", - "LibraryEntity"."name" AS "LibraryEntity_name", - "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", - "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", - "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", - "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", - "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", - "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", - "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", - "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", - "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", - "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", - "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", - "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", - "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", - "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", - "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", - "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", - "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", - "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", - "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" -FROM - "libraries" "LibraryEntity" - LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" -WHERE - ((NOT ("LibraryEntity"."deletedAt" IS NULL))) -ORDER BY - "LibraryEntity"."createdAt" ASC +select + "libraries".*, + ( + select + to_json(obj) + from + ( + select + "users"."id", + "users"."email", + "users"."createdAt", + "users"."profileImagePath", + "users"."isAdmin", + "users"."shouldChangePassword", + "users"."deletedAt", + "users"."oauthId", + "users"."updatedAt", + "users"."storageLabel", + "users"."name", + "users"."quotaSizeInBytes", + "users"."quotaUsageInBytes", + "users"."status", + "users"."profileChangedAt" + from + "users" + where + "users"."id" = "libraries"."ownerId" + ) as obj + ) as "owner" +from + "libraries" +where + "libraries"."deletedAt" is not null +order by + "createdAt" asc -- LibraryRepository.getStatistics -SELECT - "libraries"."id" AS "libraries_id", - "libraries"."name" AS "libraries_name", - "libraries"."ownerId" AS "libraries_ownerId", - "libraries"."importPaths" AS "libraries_importPaths", - "libraries"."exclusionPatterns" AS "libraries_exclusionPatterns", - "libraries"."createdAt" AS "libraries_createdAt", - "libraries"."updatedAt" AS "libraries_updatedAt", - "libraries"."deletedAt" AS "libraries_deletedAt", - "libraries"."refreshedAt" AS "libraries_refreshedAt", - COUNT("assets"."id") FILTER ( - WHERE - "assets"."type" = 'IMAGE' - AND "assets"."isVisible" - ) AS "photos", - COUNT("assets"."id") FILTER ( - WHERE - "assets"."type" = 'VIDEO' - AND "assets"."isVisible" - ) AS "videos", - COALESCE(SUM("exif"."fileSizeInByte"), 0) AS "usage" -FROM - "libraries" "libraries" - LEFT JOIN "assets" "assets" ON "assets"."libraryId" = "libraries"."id" - AND ("assets"."deletedAt" IS NULL) - LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" -WHERE - ("libraries"."id" = $1) - AND ("libraries"."deletedAt" IS NULL) -GROUP BY +select + count("assets"."id") filter ( + where + ( + "assets"."type" = $1 + and "assets"."isVisible" = $2 + ) + ) as "photos", + count(*) filter ( + where + ( + "assets"."type" = $3 + and "assets"."isVisible" = $4 + ) + ) as "videos", + coalesce(sum("exif"."fileSizeInByte"), $5) as "usage" +from + "libraries" + inner join "assets" on "assets"."libraryId" = "libraries"."id" + inner join "exif" on "exif"."assetId" = "assets"."id" +where + "libraries"."id" = $6 +group by "libraries"."id" diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index e3945ca028..3144f314dd 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -1,10 +1,85 @@ -- NOTE: This file is auto generated by ./sql-generator +-- MemoryRepository.search +select + * +from + "memories" +where + "ownerId" = $1 +order by + "memoryAt" desc + +-- MemoryRepository.get +select + "memories".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "assets".* + from + "assets" + inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + where + "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."deletedAt" is null + ) as agg + ) as "assets" +from + "memories" +where + "id" = $1 + and "deletedAt" is null + +-- MemoryRepository.update +update "memories" +set + "ownerId" = $1, + "isSaved" = $2 +where + "id" = $3 +select + "memories".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "assets".* + from + "assets" + inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + where + "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."deletedAt" is null + ) as agg + ) as "assets" +from + "memories" +where + "id" = $1 + and "deletedAt" is null + +-- MemoryRepository.delete +delete from "memories" +where + "id" = $1 + -- MemoryRepository.getAssetIds -SELECT - "memories_assets"."assetsId" AS "assetId" -FROM - "memories_assets_assets" "memories_assets" -WHERE - "memories_assets"."memoriesId" = $1 - AND "memories_assets"."assetsId" IN ($2) +select + "assetsId" +from + "memories_assets_assets" +where + "memoriesId" = $1 + and "assetsId" in ($2) + +-- MemoryRepository.addAssetIds +insert into + "memories_assets_assets" ("memoriesId", "assetsId") +values + ($1, $2) diff --git a/server/src/queries/partner.repository.sql b/server/src/queries/partner.repository.sql new file mode 100644 index 0000000000..e115dc34b9 --- /dev/null +++ b/server/src/queries/partner.repository.sql @@ -0,0 +1,189 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- PartnerRepository.getAll +select + "partners".*, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" +from + "partners" + inner join "users" as "sharedBy" on "partners"."sharedById" = "sharedBy"."id" + and "sharedBy"."deletedAt" is null + inner join "users" as "sharedWith" on "partners"."sharedWithId" = "sharedWith"."id" + and "sharedWith"."deletedAt" is null +where + ( + "sharedWithId" = $1 + or "sharedById" = $2 + ) + +-- PartnerRepository.get +select + "partners".*, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" +from + "partners" + inner join "users" as "sharedBy" on "partners"."sharedById" = "sharedBy"."id" + and "sharedBy"."deletedAt" is null + inner join "users" as "sharedWith" on "partners"."sharedWithId" = "sharedWith"."id" + and "sharedWith"."deletedAt" is null +where + "sharedWithId" = $1 + and "sharedById" = $2 + +-- PartnerRepository.create +insert into + "partners" ("sharedWithId", "sharedById") +values + ($1, $2) +returning + *, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" + +-- PartnerRepository.update +update "partners" +set + "inTimeline" = $1 +where + "sharedWithId" = $2 + and "sharedById" = $3 +returning + *, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" + +-- PartnerRepository.remove +delete from "partners" +where + "sharedWithId" = $1 + and "sharedById" = $2 diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql new file mode 100644 index 0000000000..f7da019f05 --- /dev/null +++ b/server/src/queries/stack.repository.sql @@ -0,0 +1,257 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- StackRepository.search +SELECT + "StackEntity"."id" AS "StackEntity_id", + "StackEntity"."ownerId" AS "StackEntity_ownerId", + "StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId", + "StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id", + "StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId", + "StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId", + "StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId", + "StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId", + "StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type", + "StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status", + "StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath", + "StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash", + "StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath", + "StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt", + "StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt", + "StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt", + "StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt", + "StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime", + "StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt", + "StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite", + "StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived", + "StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal", + "StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline", + "StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum", + "StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration", + "StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible", + "StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId", + "StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName", + "StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath", + "StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId", + "StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps" +FROM + "asset_stack" "StackEntity" + LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id" + AND ( + "StackEntity__StackEntity_assets"."deletedAt" IS NULL + ) + LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id" +WHERE + (("StackEntity"."ownerId" = $1)) + +-- StackRepository.delete +SELECT DISTINCT + "distinctAlias"."StackEntity_id" AS "ids_StackEntity_id", + "distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" +FROM + ( + SELECT + "StackEntity"."id" AS "StackEntity_id", + "StackEntity"."ownerId" AS "StackEntity_ownerId", + "StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId", + "StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id", + "StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId", + "StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId", + "StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId", + "StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId", + "StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type", + "StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status", + "StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath", + "StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash", + "StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath", + "StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt", + "StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt", + "StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt", + "StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt", + "StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime", + "StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt", + "StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite", + "StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived", + "StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal", + "StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline", + "StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum", + "StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration", + "StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible", + "StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId", + "StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName", + "StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath", + "StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId", + "StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_id", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."value" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_value", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."createdAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_createdAt", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."updatedAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_updatedAt", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."color" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_color", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."parentId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_parentId", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."userId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_userId" + FROM + "asset_stack" "StackEntity" + LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id" + AND ( + "StackEntity__StackEntity_assets"."deletedAt" IS NULL + ) + LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id" + LEFT JOIN "tag_asset" "4f1c9474d4596aede2814ee2eb938eecf7a93b95" ON "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."assetsId" = "StackEntity__StackEntity_assets"."id" + LEFT JOIN "tags" "3e3064f11b97177a1e1ce3c77ecf32850343aba1" ON "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" = "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."tagsId" + WHERE + (("StackEntity"."id" = $1)) + ) "distinctAlias" +ORDER BY + "distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" ASC, + "StackEntity_id" ASC +LIMIT + 1 + +-- StackRepository.getById +SELECT DISTINCT + "distinctAlias"."StackEntity_id" AS "ids_StackEntity_id", + "distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" +FROM + ( + SELECT + "StackEntity"."id" AS "StackEntity_id", + "StackEntity"."ownerId" AS "StackEntity_ownerId", + "StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId", + "StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id", + "StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId", + "StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId", + "StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId", + "StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId", + "StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type", + "StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status", + "StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath", + "StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash", + "StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath", + "StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt", + "StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt", + "StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt", + "StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt", + "StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime", + "StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt", + "StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite", + "StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived", + "StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal", + "StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline", + "StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum", + "StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration", + "StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible", + "StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId", + "StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName", + "StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath", + "StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId", + "StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating", + "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_id", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."value" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_value", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."createdAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_createdAt", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."updatedAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_updatedAt", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."color" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_color", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."parentId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_parentId", + "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."userId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_userId" + FROM + "asset_stack" "StackEntity" + LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id" + AND ( + "StackEntity__StackEntity_assets"."deletedAt" IS NULL + ) + LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id" + LEFT JOIN "tag_asset" "4f1c9474d4596aede2814ee2eb938eecf7a93b95" ON "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."assetsId" = "StackEntity__StackEntity_assets"."id" + LEFT JOIN "tags" "3e3064f11b97177a1e1ce3c77ecf32850343aba1" ON "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" = "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."tagsId" + WHERE + (("StackEntity"."id" = $1)) + ) "distinctAlias" +ORDER BY + "distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" ASC, + "StackEntity_id" ASC +LIMIT + 1 diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index f3cbf392db..15288b94fa 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -1,21 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Kysely, sql } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/db'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; -import { ActivityEntity } from 'src/entities/activity.entity'; -import { AlbumEntity } from 'src/entities/album.entity'; -import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { LibraryEntity } from 'src/entities/library.entity'; -import { MemoryEntity } from 'src/entities/memory.entity'; -import { PartnerEntity } from 'src/entities/partner.entity'; -import { PersonEntity } from 'src/entities/person.entity'; -import { SessionEntity } from 'src/entities/session.entity'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { StackEntity } from 'src/entities/stack.entity'; -import { TagEntity } from 'src/entities/tag.entity'; + import { AlbumUserRole } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; -import { Brackets, In, Repository } from 'typeorm'; +import { asUuid } from 'src/utils/database'; type IActivityAccess = IAccessRepository['activity']; type IAlbumAccess = IAccessRepository['album']; @@ -30,10 +21,7 @@ type ITimelineAccess = IAccessRepository['timeline']; @Injectable() class ActivityAccess implements IActivityAccess { - constructor( - private activityRepository: Repository, - private albumRepository: Repository, - ) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -42,15 +30,16 @@ class ActivityAccess implements IActivityAccess { return new Set(); } - return this.activityRepository - .find({ - select: { id: true }, - where: { - id: In([...activityIds]), - userId, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))); + return this.db + .selectFrom('activity') + .select('activity.id') + .where('activity.id', 'in', [...activityIds]) + .where('activity.userId', '=', userId) + .execute() + .then((activities) => { + console.log('activities', activities); + return new Set(activities.map((activity) => activity.id)); + }); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @@ -60,16 +49,13 @@ class ActivityAccess implements IActivityAccess { return new Set(); } - return this.activityRepository - .find({ - select: { id: true }, - where: { - id: In([...activityIds]), - album: { - ownerId: userId, - }, - }, - }) + return this.db + .selectFrom('activity') + .select('activity.id') + .leftJoin('albums', (join) => join.onRef('activity.albumId', '=', 'albums.id').on('albums.deletedAt', 'is', null)) + .where('activity.id', 'in', [...activityIds]) + .whereRef('albums.ownerId', '=', asUuid(userId)) + .execute() .then((activities) => new Set(activities.map((activity) => activity.id))); } @@ -80,28 +66,22 @@ class ActivityAccess implements IActivityAccess { return new Set(); } - return this.albumRepository - .createQueryBuilder('album') - .select('album.id') - .leftJoin('album.albumUsers', 'album_albumUsers_users') - .leftJoin('album_albumUsers_users.user', 'albumUsers') - .where('album.id IN (:...albumIds)', { albumIds: [...albumIds] }) - .andWhere('album.isActivityEnabled = true') - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('albumUsers.id = :userId', { userId }); - }), - ) - .getMany() + return this.db + .selectFrom('albums') + .select('albums.id') + .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id') + .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null)) + .where('albums.id', 'in', [...albumIds]) + .where('albums.isActivityEnabled', '=', true) + .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('users.id', '=', userId)])) + .where('albums.deletedAt', 'is', null) + .execute() .then((albums) => new Set(albums.map((album) => album.id))); } } class AlbumAccess implements IAlbumAccess { - constructor( - private albumRepository: Repository, - private sharedLinkRepository: Repository, - ) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -110,14 +90,13 @@ class AlbumAccess implements IAlbumAccess { return new Set(); } - return this.albumRepository - .find({ - select: { id: true }, - where: { - id: In([...albumIds]), - ownerId: userId, - }, - }) + return this.db + .selectFrom('albums') + .select('albums.id') + .where('albums.id', 'in', [...albumIds]) + .where('albums.ownerId', '=', userId) + .where('albums.deletedAt', 'is', null) + .execute() .then((albums) => new Set(albums.map((album) => album.id))); } @@ -128,19 +107,19 @@ class AlbumAccess implements IAlbumAccess { return new Set(); } - return this.albumRepository - .find({ - select: { id: true }, - where: { - id: In([...albumIds]), - albumUsers: { - user: { id: userId }, - // If editor access is needed we check for it, otherwise both are accepted - role: - access === AlbumUserRole.EDITOR ? AlbumUserRole.EDITOR : In([AlbumUserRole.EDITOR, AlbumUserRole.VIEWER]), - }, - }, - }) + const accessRole = + access === AlbumUserRole.EDITOR ? [AlbumUserRole.EDITOR] : [AlbumUserRole.EDITOR, AlbumUserRole.VIEWER]; + + return this.db + .selectFrom('albums') + .select('albums.id') + .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id') + .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null)) + .where('albums.id', 'in', [...albumIds]) + .where('albums.deletedAt', 'is', null) + .where('users.id', '=', userId) + .where('albumUsers.role', 'in', [...accessRole]) + .execute() .then((albums) => new Set(albums.map((album) => album.id))); } @@ -151,14 +130,12 @@ class AlbumAccess implements IAlbumAccess { return new Set(); } - return this.sharedLinkRepository - .find({ - select: { albumId: true }, - where: { - id: sharedLinkId, - albumId: In([...albumIds]), - }, - }) + return this.db + .selectFrom('shared_links') + .select('shared_links.albumId') + .where('shared_links.id', '=', sharedLinkId) + .where('shared_links.albumId', 'in', [...albumIds]) + .execute() .then( (sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), ); @@ -166,12 +143,7 @@ class AlbumAccess implements IAlbumAccess { } class AssetAccess implements IAssetAccess { - constructor( - private albumRepository: Repository, - private assetRepository: Repository, - private partnerRepository: Repository, - private sharedLinkRepository: Repository, - ) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -180,30 +152,31 @@ class AssetAccess implements IAssetAccess { return new Set(); } - return this.albumRepository - .createQueryBuilder('album') - .innerJoin('album.assets', 'asset') - .leftJoin('album.albumUsers', 'album_albumUsers_users') - .leftJoin('album_albumUsers_users.user', 'albumUsers') - .select('asset.id', 'assetId') - .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') - .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { - assetIds: [...assetIds], - }) - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('albumUsers.id = :userId', { userId }); - }), + return this.db + .selectFrom('albums') + .innerJoin('albums_assets_assets as albumAssets', 'albums.id', 'albumAssets.albumsId') + .innerJoin('assets', (join) => + join.onRef('assets.id', '=', 'albumAssets.assetsId').on('assets.deletedAt', 'is', null), ) - .getRawMany() - .then((rows) => { + .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id') + .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null)) + .select(['assets.id', 'assets.livePhotoVideoId']) + .where( + sql`array["assets"."id", "assets"."livePhotoVideoId"]`, + '&&', + sql`array[${sql.join([...assetIds])}]::uuid[] `, + ) + .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('users.id', '=', userId)])) + .where('albums.deletedAt', 'is', null) + .execute() + .then((assets) => { const allowedIds = new Set(); - for (const row of rows) { - if (row.assetId && assetIds.has(row.assetId)) { - allowedIds.add(row.assetId); + for (const asset of assets) { + if (asset.id && assetIds.has(asset.id)) { + allowedIds.add(asset.id); } - if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { - allowedIds.add(row.livePhotoVideoId); + if (asset.livePhotoVideoId && assetIds.has(asset.livePhotoVideoId)) { + allowedIds.add(asset.livePhotoVideoId); } } return allowedIds; @@ -217,15 +190,12 @@ class AssetAccess implements IAssetAccess { return new Set(); } - return this.assetRepository - .find({ - select: { id: true }, - where: { - id: In([...assetIds]), - ownerId: userId, - }, - withDeleted: true, - }) + return this.db + .selectFrom('assets') + .select('assets.id') + .where('assets.id', 'in', [...assetIds]) + .where('assets.ownerId', '=', userId) + .execute() .then((assets) => new Set(assets.map((asset) => asset.id))); } @@ -236,16 +206,20 @@ class AssetAccess implements IAssetAccess { return new Set(); } - return this.partnerRepository - .createQueryBuilder('partner') - .innerJoin('partner.sharedBy', 'sharedBy') - .innerJoin('sharedBy.assets', 'asset') - .select('asset.id', 'assetId') - .where('partner.sharedWithId = :userId', { userId }) - .andWhere('asset.isArchived = false') - .andWhere('asset.id IN (:...assetIds)', { assetIds: [...assetIds] }) - .getRawMany() - .then((rows) => new Set(rows.map((row) => row.assetId))); + return this.db + .selectFrom('partners as partner') + .innerJoin('users as sharedBy', (join) => + join.onRef('sharedBy.id', '=', 'partner.sharedById').on('sharedBy.deletedAt', 'is', null), + ) + .innerJoin('assets', (join) => + join.onRef('assets.ownerId', '=', 'sharedBy.id').on('assets.deletedAt', 'is', null), + ) + .select('assets.id') + .where('partner.sharedWithId', '=', userId) + .where('assets.isArchived', '=', false) + .where('assets.id', 'in', [...assetIds]) + .execute() + .then((assets) => new Set(assets.map((asset) => asset.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @@ -255,23 +229,32 @@ class AssetAccess implements IAssetAccess { return new Set(); } - return this.sharedLinkRepository - .createQueryBuilder('sharedLink') - .leftJoin('sharedLink.album', 'album') - .leftJoin('sharedLink.assets', 'assets') - .leftJoin('album.assets', 'albumAssets') - .select('assets.id', 'assetId') - .addSelect('albumAssets.id', 'albumAssetId') - .addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId') - .addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId') - .where('sharedLink.id = :sharedLinkId', { sharedLinkId }) - .andWhere( - 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', - { - assetIds: [...assetIds], - }, + return this.db + .selectFrom('shared_links') + .leftJoin('albums', (join) => + join.onRef('albums.id', '=', 'shared_links.albumId').on('albums.deletedAt', 'is', null), ) - .getRawMany() + .leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id') + .leftJoin('assets', (join) => + join.onRef('assets.id', '=', 'shared_link__asset.assetsId').on('assets.deletedAt', 'is', null), + ) + .leftJoin('albums_assets_assets', 'albums_assets_assets.albumsId', 'albums.id') + .leftJoin('assets as albumAssets', (join) => + join.onRef('albumAssets.id', '=', 'albums_assets_assets.assetsId').on('albumAssets.deletedAt', 'is', null), + ) + .select([ + 'assets.id as assetId', + 'assets.livePhotoVideoId as assetLivePhotoVideoId', + 'albumAssets.id as albumAssetId', + 'albumAssets.livePhotoVideoId as albumAssetLivePhotoVideoId', + ]) + .where('shared_links.id', '=', sharedLinkId) + .where( + sql`array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"]`, + '&&', + sql`array[${sql.join([...assetIds])}]::uuid[] `, + ) + .execute() .then((rows) => { const allowedIds = new Set(); for (const row of rows) { @@ -294,7 +277,7 @@ class AssetAccess implements IAssetAccess { } class AuthDeviceAccess implements IAuthDeviceAccess { - constructor(private sessionRepository: Repository) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -303,20 +286,18 @@ class AuthDeviceAccess implements IAuthDeviceAccess { return new Set(); } - return this.sessionRepository - .find({ - select: { id: true }, - where: { - userId, - id: In([...deviceIds]), - }, - }) + return this.db + .selectFrom('sessions') + .select('sessions.id') + .where('sessions.userId', '=', userId) + .where('sessions.id', 'in', [...deviceIds]) + .execute() .then((tokens) => new Set(tokens.map((token) => token.id))); } } class StackAccess implements IStackAccess { - constructor(private stackRepository: Repository) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -325,20 +306,18 @@ class StackAccess implements IStackAccess { return new Set(); } - return this.stackRepository - .find({ - select: { id: true }, - where: { - id: In([...stackIds]), - ownerId: userId, - }, - }) + return this.db + .selectFrom('asset_stack as stacks') + .select('stacks.id') + .where('stacks.id', 'in', [...stackIds]) + .where('stacks.ownerId', '=', userId) + .execute() .then((stacks) => new Set(stacks.map((stack) => stack.id))); } } class TimelineAccess implements ITimelineAccess { - constructor(private partnerRepository: Repository) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -347,18 +326,18 @@ class TimelineAccess implements ITimelineAccess { return new Set(); } - return this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() + return this.db + .selectFrom('partners') + .select('partners.sharedById') + .where('partners.sharedById', 'in', [...partnerIds]) + .where('partners.sharedWithId', '=', userId) + .execute() .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } class MemoryAccess implements IMemoryAccess { - constructor(private memoryRepository: Repository) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -367,23 +346,19 @@ class MemoryAccess implements IMemoryAccess { return new Set(); } - return this.memoryRepository - .find({ - select: { id: true }, - where: { - id: In([...memoryIds]), - ownerId: userId, - }, - }) + return this.db + .selectFrom('memories') + .select('memories.id') + .where('memories.id', 'in', [...memoryIds]) + .where('memories.ownerId', '=', userId) + .where('memories.deletedAt', 'is', null) + .execute() .then((memories) => new Set(memories.map((memory) => memory.id))); } } class PersonAccess implements IPersonAccess { - constructor( - private assetFaceRepository: Repository, - private personRepository: Repository, - ) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -392,14 +367,12 @@ class PersonAccess implements IPersonAccess { return new Set(); } - return this.personRepository - .find({ - select: { id: true }, - where: { - id: In([...personIds]), - ownerId: userId, - }, - }) + return this.db + .selectFrom('person') + .select('person.id') + .where('person.id', 'in', [...personIds]) + .where('person.ownerId', '=', userId) + .execute() .then((persons) => new Set(persons.map((person) => person.id))); } @@ -410,22 +383,21 @@ class PersonAccess implements IPersonAccess { return new Set(); } - return this.assetFaceRepository - .find({ - select: { id: true }, - where: { - id: In([...assetFaceIds]), - asset: { - ownerId: userId, - }, - }, - }) + return this.db + .selectFrom('asset_faces') + .select('asset_faces.id') + .leftJoin('assets', (join) => + join.onRef('assets.id', '=', 'asset_faces.assetId').on('assets.deletedAt', 'is', null), + ) + .where('asset_faces.id', 'in', [...assetFaceIds]) + .where('assets.ownerId', '=', userId) + .execute() .then((faces) => new Set(faces.map((face) => face.id))); } } class PartnerAccess implements IPartnerAccess { - constructor(private partnerRepository: Repository) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -434,18 +406,18 @@ class PartnerAccess implements IPartnerAccess { return new Set(); } - return this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() + return this.db + .selectFrom('partners') + .select('partners.sharedById') + .where('partners.sharedById', 'in', [...partnerIds]) + .where('partners.sharedWithId', '=', userId) + .execute() .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } class TagAccess implements ITagAccess { - constructor(private tagRepository: Repository) {} + constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) @@ -454,14 +426,12 @@ class TagAccess implements ITagAccess { return new Set(); } - return this.tagRepository - .find({ - select: { id: true }, - where: { - id: In([...tagIds]), - userId, - }, - }) + return this.db + .selectFrom('tags') + .select('tags.id') + .where('tags.id', 'in', [...tagIds]) + .where('tags.userId', '=', userId) + .execute() .then((tags) => new Set(tags.map((tag) => tag.id))); } } @@ -478,29 +448,16 @@ export class AccessRepository implements IAccessRepository { tag: ITagAccess; timeline: ITimelineAccess; - constructor( - @InjectRepository(ActivityEntity) activityRepository: Repository, - @InjectRepository(AssetEntity) assetRepository: Repository, - @InjectRepository(AlbumEntity) albumRepository: Repository, - @InjectRepository(LibraryEntity) libraryRepository: Repository, - @InjectRepository(MemoryEntity) memoryRepository: Repository, - @InjectRepository(PartnerEntity) partnerRepository: Repository, - @InjectRepository(PersonEntity) personRepository: Repository, - @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, - @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, - @InjectRepository(SessionEntity) sessionRepository: Repository, - @InjectRepository(StackEntity) stackRepository: Repository, - @InjectRepository(TagEntity) tagRepository: Repository, - ) { - this.activity = new ActivityAccess(activityRepository, albumRepository); - this.album = new AlbumAccess(albumRepository, sharedLinkRepository); - this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); - this.authDevice = new AuthDeviceAccess(sessionRepository); - this.memory = new MemoryAccess(memoryRepository); - this.person = new PersonAccess(assetFaceRepository, personRepository); - this.partner = new PartnerAccess(partnerRepository); - this.stack = new StackAccess(stackRepository); - this.tag = new TagAccess(tagRepository); - this.timeline = new TimelineAccess(partnerRepository); + constructor(@InjectKysely() db: Kysely) { + this.activity = new ActivityAccess(db); + this.album = new AlbumAccess(db); + this.asset = new AssetAccess(db); + this.authDevice = new AuthDeviceAccess(db); + this.memory = new MemoryAccess(db); + this.person = new PersonAccess(db); + this.partner = new PartnerAccess(db); + this.stack = new StackAccess(db); + this.tag = new TagAccess(db); + this.timeline = new TimelineAccess(db); } } diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index 6ed82abdfc..99d3192341 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -2,10 +2,9 @@ import { Injectable } from '@nestjs/common'; import { ExpressionBuilder, Insertable, Kysely } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; +import { columns } from 'src/database'; import { Activity, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { ActivityEntity } from 'src/entities/activity.entity'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { asUuid } from 'src/utils/database'; export interface ActivitySearch { @@ -19,18 +18,18 @@ const withUser = (eb: ExpressionBuilder) => { return jsonObjectFrom( eb .selectFrom('users') - .selectAll() + .select(columns.userDto) .whereRef('users.id', '=', 'activity.userId') .where('users.deletedAt', 'is', null), ).as('user'); }; @Injectable() -export class ActivityRepository implements IActivityRepository { +export class ActivityRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [{ albumId: DummyValue.UUID }] }) - search(options: ActivitySearch): Promise { + search(options: ActivitySearch) { const { userId, assetId, albumId, isLiked } = options; return this.db @@ -44,14 +43,14 @@ export class ActivityRepository implements IActivityRepository { .$if(!!albumId, (qb) => qb.where('activity.albumId', '=', albumId!)) .$if(isLiked !== undefined, (qb) => qb.where('activity.isLiked', '=', isLiked!)) .orderBy('activity.createdAt', 'asc') - .execute() as unknown as Promise; + .execute(); } async create(activity: Insertable) { return this.save(activity); } - async delete(id: string): Promise { + async delete(id: string) { await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute(); } @@ -79,6 +78,6 @@ export class ActivityRepository implements IActivityRepository { .selectAll('activity') .select(withUser) .where('activity.id', '=', asUuid(id)) - .executeTakeFirstOrThrow() as unknown as Promise; + .executeTakeFirstOrThrow(); } } diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index 9328ea8cfc..5895721b63 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -1,26 +1,40 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AlbumUserEntity } from 'src/entities/album-user.entity'; +import { Insertable, Kysely, Selectable, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { AlbumsSharedUsersUsers, DB } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AlbumUserRole } from 'src/enum'; import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface'; -import { Repository } from 'typeorm'; @Injectable() export class AlbumUserRepository implements IAlbumUserRepository { - constructor(@InjectRepository(AlbumUserEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} - async create(albumUser: Partial): Promise { - const { userId, albumId } = await this.repository.save(albumUser); - return this.repository.findOneOrFail({ where: { userId, albumId } }); + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) + create(albumUser: Insertable): Promise> { + return this.db.insertInto('albums_shared_users_users').values(albumUser).returningAll().executeTakeFirstOrThrow(); } - async update({ userId, albumId }: AlbumPermissionId, dto: Partial): Promise { - await this.repository.update({ userId, albumId }, dto); - return this.repository.findOneOrFail({ - where: { userId, albumId }, - }); + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) + update( + { usersId, albumsId }: AlbumPermissionId, + dto: Updateable, + ): Promise> { + return this.db + .updateTable('albums_shared_users_users') + .set(dto) + .where('usersId', '=', usersId) + .where('albumsId', '=', albumsId) + .returningAll() + .executeTakeFirstOrThrow(); } - async delete({ userId, albumId }: AlbumPermissionId): Promise { - await this.repository.delete({ userId, albumId }); + @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) + async delete({ usersId, albumsId }: AlbumPermissionId): Promise { + await this.db + .deleteFrom('albums_shared_users_users') + .where('usersId', '=', usersId) + .where('albumsId', '=', albumsId) + .execute(); } } diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 8c3a2549f0..19068ddc5d 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -83,7 +83,7 @@ describe('getEnv', () => { config: { kysely: { dialect: expect.any(PostgresJSDialect), - log: ['error'], + log: expect.any(Function), }, typeorm: expect.objectContaining({ type: 'postgres', diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 15775d0471..67699880bd 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -5,7 +5,7 @@ import { Request, Response } from 'express'; import { PostgresJSDialect } from 'kysely-postgres-js'; import { CLS_ID } from 'nestjs-cls'; import { join, resolve } from 'node:path'; -import postgres from 'postgres'; +import postgres, { Notice } from 'postgres'; import { citiesFile, excludePaths, IWorker } from 'src/constants'; import { Telemetry } from 'src/decorators'; import { EnvDto } from 'src/dtos/env.dto'; @@ -99,6 +99,11 @@ const getEnv = (): EnvData => { } const driverOptions = { + onnotice: (notice: Notice) => { + if (notice['severity'] !== 'NOTICE') { + console.warn('Postgres notice:', notice); + } + }, max: 10, types: { date: { @@ -194,7 +199,16 @@ const getEnv = (): EnvData => { dialect: new PostgresJSDialect({ postgres: databaseUrl ? postgres(databaseUrl, driverOptions) : postgres({ ...parts, ...driverOptions }), }), - log: ['error'] as const, + log(event) { + if (event.level === 'error') { + console.error('Query failed :', { + durationMs: event.queryDurationMillis, + error: event.error, + sql: event.query.sql, + params: event.query.parameters, + }); + } + }, }, }, diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index eb6a5d6f71..c48233f08f 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -1,5 +1,4 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; @@ -78,8 +77,12 @@ import { VersionHistoryRepository } from 'src/repositories/version-history.repos import { ViewRepository } from 'src/repositories/view-repository'; export const repositories = [ + // + ActivityRepository, +]; + +export const providers = [ { provide: IAccessRepository, useClass: AccessRepository }, - { provide: IActivityRepository, useClass: ActivityRepository }, { provide: IAlbumRepository, useClass: AlbumRepository }, { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 1446395854..ca279d7dea 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -1,84 +1,122 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { ExpressionBuilder, Insertable, Kysely, Updateable } from 'kysely'; +import { jsonObjectFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Libraries } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; import { LibraryEntity } from 'src/entities/library.entity'; +import { AssetType } from 'src/enum'; import { ILibraryRepository } from 'src/interfaces/library.interface'; -import { IsNull, Not } from 'typeorm'; -import { Repository } from 'typeorm/repository/Repository.js'; + +const userColumns = [ + 'users.id', + 'users.email', + 'users.createdAt', + 'users.profileImagePath', + 'users.isAdmin', + 'users.shouldChangePassword', + 'users.deletedAt', + 'users.oauthId', + 'users.updatedAt', + 'users.storageLabel', + 'users.name', + 'users.quotaSizeInBytes', + 'users.quotaUsageInBytes', + 'users.status', + 'users.profileChangedAt', +] as const; + +const withOwner = (eb: ExpressionBuilder) => { + return jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'libraries.ownerId').select(userColumns)).as( + 'owner', + ); +}; @Injectable() export class LibraryRepository implements ILibraryRepository { - constructor(@InjectRepository(LibraryEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID] }) - get(id: string, withDeleted = false): Promise { - return this.repository.findOneOrFail({ - where: { - id, - }, - relations: { owner: true }, - withDeleted, - }); + get(id: string, withDeleted = false): Promise { + return this.db + .selectFrom('libraries') + .selectAll('libraries') + .select(withOwner) + .where('libraries.id', '=', id) + .$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null)) + .executeTakeFirst() as Promise; } @GenerateSql({ params: [] }) getAll(withDeleted = false): Promise { - return this.repository.find({ - relations: { - owner: true, - }, - order: { - createdAt: 'ASC', - }, - withDeleted, - }); + return this.db + .selectFrom('libraries') + .selectAll('libraries') + .select(withOwner) + .orderBy('createdAt', 'asc') + .$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null)) + .execute() as unknown as Promise; } @GenerateSql() getAllDeleted(): Promise { - return this.repository.find({ - where: { - deletedAt: Not(IsNull()), - }, - relations: { - owner: true, - }, - order: { - createdAt: 'ASC', - }, - withDeleted: true, - }); + return this.db + .selectFrom('libraries') + .selectAll('libraries') + .select(withOwner) + .where('libraries.deletedAt', 'is not', null) + .orderBy('createdAt', 'asc') + .execute() as unknown as Promise; } - create(library: Omit): Promise { - return this.repository.save(library); + create(library: Insertable): Promise { + return this.db + .insertInto('libraries') + .values(library) + .returningAll() + .executeTakeFirstOrThrow() as Promise; } async delete(id: string): Promise { - await this.repository.delete({ id }); + await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute(); } async softDelete(id: string): Promise { - await this.repository.softDelete({ id }); + await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute(); } - async update(library: Partial): Promise { - return this.save(library); + update(id: string, library: Updateable): Promise { + return this.db + .updateTable('libraries') + .set(library) + .where('libraries.id', '=', id) + .returningAll() + .executeTakeFirstOrThrow() as Promise; } @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(id: string): Promise { - const stats = await this.repository - .createQueryBuilder('libraries') - .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') - .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos') - .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage') - .leftJoin('libraries.assets', 'assets') - .leftJoin('assets.exifInfo', 'exif') + const stats = await this.db + .selectFrom('libraries') + .innerJoin('assets', 'assets.libraryId', 'libraries.id') + .innerJoin('exif', 'exif.assetId', 'assets.id') + .select((eb) => + eb.fn + .count('assets.id') + .filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)])) + .as('photos'), + ) + .select((eb) => + eb.fn + .countAll() + .filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)])) + .as('videos'), + ) + .select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('exif.fileSizeInByte'), eb.val(0)).as('usage')) .groupBy('libraries.id') - .where('libraries.id = :id', { id }) - .getRawOne(); + .where('libraries.id', '=', id) + .executeTakeFirst(); if (!stats) { return; @@ -91,9 +129,4 @@ export class LibraryRepository implements ILibraryRepository { total: Number(stats.photos) + Number(stats.videos), }; } - - private async save(library: Partial) { - const { id } = await this.repository.save(library); - return this.repository.findOneByOrFail({ id }); - } } diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index b9d17686a1..00870e78eb 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -1,17 +1,15 @@ import { Inject, Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { getName } from 'i18n-iso-countries'; +import { Expression, Kysely, sql, SqlBool } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; import { randomUUID } from 'node:crypto'; import { createReadStream, existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import readLine from 'node:readline'; import { citiesFile } from 'src/constants'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { GeodataPlacesEntity, GeodataPlacesTempEntity } from 'src/entities/geodata-places.entity'; -import { - NaturalEarthCountriesEntity, - NaturalEarthCountriesTempEntity, -} from 'src/entities/natural-earth-countries.entity'; +import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; +import { AssetEntity, withExif } from 'src/entities/asset.entity'; +import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity'; import { LogLevel, SystemMetadataKey } from 'src/enum'; import { IConfigRepository } from 'src/interfaces/config.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -23,21 +21,19 @@ import { ReverseGeocodeResult, } from 'src/interfaces/map.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { OptionalBetween } from 'src/utils/database'; -import { DataSource, In, IsNull, Not, Repository } from 'typeorm'; -import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; + +interface MapDB extends DB { + geodata_places_tmp: GeodataPlaces; + naturalearth_countries_tmp: NaturalearthCountries; +} @Injectable() export class MapRepository implements IMapRepository { constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository, - @InjectRepository(NaturalEarthCountriesEntity) - private naturalEarthCountriesRepository: Repository, - @InjectDataSource() private dataSource: DataSource, @Inject(IConfigRepository) private configRepository: IConfigRepository, @Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, + @InjectKysely() private db: Kysely, ) { this.logger.setContext(MapRepository.name); } @@ -70,39 +66,34 @@ export class MapRepository implements IMapRepository { ): Promise { const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options; - const where = { - isVisible: true, - isArchived, - exifInfo: { - latitude: Not(IsNull()), - longitude: Not(IsNull()), - }, - isFavorite, - fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore), - }; + const assets = (await this.db + .selectFrom('assets') + .$call(withExif) + .select('id') + .leftJoin('albums_assets_assets', (join) => join.onRef('assets.id', '=', 'albums_assets_assets.assetsId')) + .where('isVisible', '=', true) + .$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!)) + .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) + .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) + .$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!)) + .where('deletedAt', 'is', null) + .where('exif.latitude', 'is not', null) + .where('exif.longitude', 'is not', null) + .where((eb) => { + const ors: Expression[] = []; - const assets = await this.assetRepository.find({ - select: { - id: true, - exifInfo: { - city: true, - state: true, - country: true, - latitude: true, - longitude: true, - }, - }, - where: [ - { ...where, ownerId: In([...ownerIds]) }, - { ...where, albums: { id: In([...albumIds]) } }, - ], - relations: { - exifInfo: true, - }, - order: { - fileCreatedAt: 'DESC', - }, - }); + if (ownerIds.length > 0) { + ors.push(eb('ownerId', 'in', ownerIds)); + } + + if (albumIds.length > 0) { + ors.push(eb('albums_assets_assets.albumsId', 'in', albumIds)); + } + + return eb.or(ors); + }) + .orderBy('fileCreatedAt', 'desc') + .execute()) as any as AssetEntity[]; return assets.map((asset) => ({ id: asset.id, @@ -117,15 +108,19 @@ export class MapRepository implements IMapRepository { async reverseGeocode(point: GeoPoint): Promise { this.logger.debug(`Request: ${point.latitude},${point.longitude}`); - const response = await this.geodataPlacesRepository - .createQueryBuilder('geoplaces') + const response = await this.db + .selectFrom('geodata_places') + .selectAll() .where( - 'earth_box(ll_to_earth_public(:latitude, :longitude), 25000) @> ll_to_earth_public(latitude, longitude)', - point, + sql`earth_box(ll_to_earth_public(${point.latitude}, ${point.longitude}), 25000)`, + '@>', + sql`ll_to_earth_public(latitude, longitude)`, + ) + .orderBy( + sql`(earth_distance(ll_to_earth_public(${point.latitude}, ${point.longitude}), ll_to_earth_public(latitude, longitude)))`, ) - .orderBy('earth_distance(ll_to_earth_public(:latitude, :longitude), ll_to_earth_public(latitude, longitude))') .limit(1) - .getOne(); + .executeTakeFirst(); if (response) { if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) { @@ -143,11 +138,12 @@ export class MapRepository implements IMapRepository { `Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`, ); - const ne_response = await this.naturalEarthCountriesRepository - .createQueryBuilder('naturalearth_countries') - .where('coordinates @> point (:longitude, :latitude)', point) + const ne_response = await this.db + .selectFrom('naturalearth_countries') + .selectAll() + .where('coordinates', '@>', sql`point(${point.longitude}, ${point.latitude})`) .limit(1) - .getOne(); + .executeTakeFirst(); if (!ne_response) { this.logger.warn( @@ -176,10 +172,11 @@ export class MapRepository implements IMapRepository { return; } - await this.dataSource.query('DROP TABLE IF EXISTS naturalearth_countries_tmp'); - await this.dataSource.query( - 'CREATE TABLE naturalearth_countries_tmp (LIKE naturalearth_countries INCLUDING ALL EXCLUDING INDEXES)', + await this.db.schema.dropTable('naturalearth_countries_tmp').ifExists().execute(); + await sql`CREATE TABLE naturalearth_countries_tmp (LIKE naturalearth_countries INCLUDING ALL EXCLUDING INDEXES)`.execute( + this.db, ); + const entities: Omit[] = []; for (const feature of geoJSONData.features) { for (const entry of feature.geometry.coordinates) { @@ -196,14 +193,14 @@ export class MapRepository implements IMapRepository { } } } - await this.dataSource.manager.insert(NaturalEarthCountriesTempEntity, entities); + await this.db.insertInto('naturalearth_countries_tmp').values(entities).execute(); - await this.dataSource.query(`ALTER TABLE naturalearth_countries_tmp ADD PRIMARY KEY (id) WITH (FILLFACTOR = 100)`); + await sql`ALTER TABLE naturalearth_countries_tmp ADD PRIMARY KEY (id) WITH (FILLFACTOR = 100)`.execute(this.db); - await this.dataSource.transaction(async (manager) => { - await manager.query('ALTER TABLE naturalearth_countries RENAME TO naturalearth_countries_old'); - await manager.query('ALTER TABLE naturalearth_countries_tmp RENAME TO naturalearth_countries'); - await manager.query('DROP TABLE naturalearth_countries_old'); + await this.db.transaction().execute(async (manager) => { + await manager.schema.alterTable('naturalearth_countries').renameTo('naturalearth_countries_old').execute(); + await manager.schema.alterTable('naturalearth_countries_tmp').renameTo('naturalearth_countries').execute(); + await manager.schema.dropTable('naturalearth_countries_old').execute(); }); } @@ -214,17 +211,15 @@ export class MapRepository implements IMapRepository { this.loadAdmin(resourcePaths.geodata.admin2), ]); - await this.dataSource.query('DROP TABLE IF EXISTS geodata_places_tmp'); - await this.dataSource.query( - 'CREATE TABLE geodata_places_tmp (LIKE geodata_places INCLUDING ALL EXCLUDING INDEXES)', - ); + await this.db.schema.dropTable('geodata_places_tmp').ifExists().execute(); + await sql`CREATE TABLE geodata_places_tmp (LIKE geodata_places INCLUDING ALL EXCLUDING INDEXES)`.execute(this.db); await this.loadCities500(admin1, admin2); await this.createGeodataIndices(); - await this.dataSource.transaction(async (manager) => { - await manager.query('ALTER TABLE geodata_places RENAME TO geodata_places_old'); - await manager.query('ALTER TABLE geodata_places_tmp RENAME TO geodata_places'); - await manager.query('DROP TABLE geodata_places_old'); + await this.db.transaction().execute(async (manager) => { + await manager.schema.alterTable('geodata_places').renameTo('geodata_places_old').execute(); + await manager.schema.alterTable('geodata_places_tmp').renameTo('geodata_places').execute(); + await manager.schema.dropTable('geodata_places_old').execute(); }); } @@ -236,7 +231,7 @@ export class MapRepository implements IMapRepository { } const input = createReadStream(cities500, { highWaterMark: 512 * 1024 * 1024 }); - let bufferGeodata: QueryDeepPartialEntity[] = []; + let bufferGeodata = []; const lineReader = readLine.createInterface({ input }); let count = 0; @@ -257,19 +252,23 @@ export class MapRepository implements IMapRepository { admin1Code: lineSplit[10], admin2Code: lineSplit[11], modificationDate: lineSplit[18], - admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`), - admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`), + admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`) ?? null, + admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`) ?? null, }; bufferGeodata.push(geoData); if (bufferGeodata.length >= 5000) { const curLength = bufferGeodata.length; futures.push( - this.dataSource.manager.insert(GeodataPlacesTempEntity, bufferGeodata).then(() => { - count += curLength; - if (count % 10_000 === 0) { - this.logger.log(`${count} geodata records imported`); - } - }), + this.db + .insertInto('geodata_places_tmp') + .values(bufferGeodata) + .execute() + .then(() => { + count += curLength; + if (count % 10_000 === 0) { + this.logger.log(`${count} geodata records imported`); + } + }), ); bufferGeodata = []; // leave spare connection for other queries @@ -280,7 +279,7 @@ export class MapRepository implements IMapRepository { } } - await this.dataSource.manager.insert(GeodataPlacesTempEntity, bufferGeodata); + await this.db.insertInto('geodata_places_tmp').values(bufferGeodata).execute(); } private async loadAdmin(filePath: string) { @@ -303,24 +302,28 @@ export class MapRepository implements IMapRepository { private createGeodataIndices() { return Promise.all([ - this.dataSource.query(`ALTER TABLE geodata_places_tmp ADD PRIMARY KEY (id) WITH (FILLFACTOR = 100)`), - this.dataSource.query(` - CREATE INDEX IDX_geodata_gist_earthcoord_${randomUUID().replaceAll('-', '_')} - ON geodata_places_tmp - USING gist (ll_to_earth_public(latitude, longitude)) - WITH (fillfactor = 100)`), - this.dataSource.query(` - CREATE INDEX idx_geodata_places_name_${randomUUID().replaceAll('-', '_')} - ON geodata_places_tmp - USING gin (f_unaccent(name) gin_trgm_ops)`), - this.dataSource.query(` - CREATE INDEX idx_geodata_places_admin1_name_${randomUUID().replaceAll('-', '_')} - ON geodata_places_tmp - USING gin (f_unaccent("admin1Name") gin_trgm_ops)`), - this.dataSource.query(` - CREATE INDEX idx_geodata_places_admin2_name_${randomUUID().replaceAll('-', '_')} - ON geodata_places_tmp - USING gin (f_unaccent("admin2Name") gin_trgm_ops)`), + sql`ALTER TABLE geodata_places_tmp ADD PRIMARY KEY (id) WITH (FILLFACTOR = 100)`.execute(this.db), + sql` + CREATE INDEX IDX_geodata_gist_earthcoord_${sql.raw(randomUUID().replaceAll('-', '_'))} + ON geodata_places_tmp + USING gist (ll_to_earth_public(latitude, longitude)) + WITH (fillfactor = 100) + `.execute(this.db), + this.db.schema + .createIndex(`idx_geodata_places_country_code_${randomUUID().replaceAll('-', '_')}`) + .on('geodata_places_tmp') + .using('gin (f_unaccent(name) gin_trgm_ops)') + .execute(), + this.db.schema + .createIndex(`idx_geodata_places_country_code_${randomUUID().replaceAll('-', '_')}`) + .on('geodata_places_tmp') + .using('gin (f_unaccent("admin1Name") gin_trgm_ops)') + .execute(), + this.db.schema + .createIndex(`idx_geodata_places_admin2_name_${randomUUID().replaceAll('-', '_')}`) + .on('geodata_places_tmp') + .using('gin (f_unaccent("admin2Name") gin_trgm_ops)') + .execute(), ]); } } diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 47dc705093..7e59b92e68 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -1,49 +1,55 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, Updateable } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Memories } from 'src/db'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { MemoryEntity } from 'src/entities/memory.entity'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; -import { DataSource, In, Repository } from 'typeorm'; @Injectable() export class MemoryRepository implements IMemoryRepository { - constructor( - @InjectRepository(MemoryEntity) private repository: Repository, - @InjectDataSource() private dataSource: DataSource, - ) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: [DummyValue.UUID] }) search(ownerId: string): Promise { - return this.repository.find({ - where: { - ownerId, - }, - order: { - memoryAt: 'DESC', - }, - }); + return this.db + .selectFrom('memories') + .selectAll() + .where('ownerId', '=', ownerId) + .orderBy('memoryAt', 'desc') + .execute() as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) get(id: string): Promise { - return this.repository.findOne({ - where: { - id, - }, - relations: { - assets: true, - }, + return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise; + } + + async create(memory: Insertable, assetIds: Set): Promise { + const id = await this.db.transaction().execute(async (tx) => { + const { id } = await tx.insertInto('memories').values(memory).returning('id').executeTakeFirstOrThrow(); + + if (assetIds.size > 0) { + const values = [...assetIds].map((assetId) => ({ memoriesId: id, assetsId: assetId })); + await tx.insertInto('memories_assets_assets').values(values).execute(); + } + + return id; }); + + return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise; } - create(memory: Partial): Promise { - return this.save(memory); - } - - update(memory: Partial): Promise { - return this.save(memory); + @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] }) + async update(id: string, memory: Updateable): Promise { + await this.db.updateTable('memories').set(memory).where('id', '=', id).execute(); + return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - await this.repository.delete({ id }); + await this.db.deleteFrom('memories').where('id', '=', id).execute(); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @@ -53,46 +59,49 @@ export class MemoryRepository implements IMemoryRepository { return new Set(); } - const results = await this.dataSource - .createQueryBuilder() - .select('memories_assets.assetsId', 'assetId') - .from('memories_assets_assets', 'memories_assets') - .where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id }) - .andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds }) - .getRawMany<{ assetId: string }>(); + const results = await this.db + .selectFrom('memories_assets_assets') + .select(['assetsId']) + .where('memoriesId', '=', id) + .where('assetsId', 'in', assetIds) + .execute(); - return new Set(results.map(({ assetId }) => assetId)); + return new Set(results.map(({ assetsId }) => assetsId)); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async addAssetIds(id: string, assetIds: string[]): Promise { - await this.dataSource - .createQueryBuilder() - .insert() - .into('memories_assets_assets', ['memoriesId', 'assetsId']) + await this.db + .insertInto('memories_assets_assets') .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) .execute(); } @Chunked({ paramIndex: 1 }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async removeAssetIds(id: string, assetIds: string[]): Promise { - await this.dataSource - .createQueryBuilder() - .delete() - .from('memories_assets_assets') - .where({ - memoriesId: id, - assetsId: In(assetIds), - }) + await this.db + .deleteFrom('memories_assets_assets') + .where('memoriesId', '=', id) + .where('assetsId', 'in', assetIds) .execute(); } - private async save(memory: Partial): Promise { - const { id } = await this.repository.save(memory); - return this.repository.findOneOrFail({ - where: { id }, - relations: { - assets: true, - }, - }); + private getByIdBuilder(id: string) { + return this.db + .selectFrom('memories') + .selectAll('memories') + .select((eb) => + jsonArrayFrom( + eb + .selectFrom('assets') + .selectAll('assets') + .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') + .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') + .where('assets.deletedAt', 'is', null), + ).as('assets'), + ) + .where('id', '=', id) + .where('deletedAt', 'is', null); } } diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts index 6b11a4e31e..929c06a1f5 100644 --- a/server/src/repositories/partner.repository.ts +++ b/server/src/repositories/partner.repository.ts @@ -1,37 +1,93 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { ExpressionBuilder, Insertable, JoinBuilder, Kysely, Updateable } from 'kysely'; +import { jsonObjectFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Partners, Users } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { PartnerEntity } from 'src/entities/partner.entity'; import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface'; -import { DeepPartial, Repository } from 'typeorm'; + +const columns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; + +const onSharedBy = (join: JoinBuilder) => + join.onRef('partners.sharedById', '=', 'sharedBy.id').on('sharedBy.deletedAt', 'is', null); + +const onSharedWith = (join: JoinBuilder) => + join.onRef('partners.sharedWithId', '=', 'sharedWith.id').on('sharedWith.deletedAt', 'is', null); + +const withSharedBy = (eb: ExpressionBuilder) => { + return jsonObjectFrom( + eb.selectFrom('users as sharedBy').select(columns).whereRef('sharedBy.id', '=', 'partners.sharedById'), + ).as('sharedBy'); +}; + +const withSharedWith = (eb: ExpressionBuilder) => { + return jsonObjectFrom( + eb.selectFrom('users as sharedWith').select(columns).whereRef('sharedWith.id', '=', 'partners.sharedWithId'), + ).as('sharedWith'); +}; @Injectable() export class PartnerRepository implements IPartnerRepository { - constructor(@InjectRepository(PartnerEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: [DummyValue.UUID] }) getAll(userId: string): Promise { - return this.repository.find({ where: [{ sharedWithId: userId }, { sharedById: userId }] }); + return this.db + .selectFrom('partners') + .innerJoin('users as sharedBy', onSharedBy) + .innerJoin('users as sharedWith', onSharedWith) + .selectAll('partners') + .select(withSharedBy) + .select(withSharedWith) + .where((eb) => eb.or([eb('sharedWithId', '=', userId), eb('sharedById', '=', userId)])) + .execute() as Promise; } - get({ sharedWithId, sharedById }: PartnerIds): Promise { - return this.repository.findOne({ where: { sharedById, sharedWithId } }); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) + get({ sharedWithId, sharedById }: PartnerIds): Promise { + return this.db + .selectFrom('partners') + .innerJoin('users as sharedBy', onSharedBy) + .innerJoin('users as sharedWith', onSharedWith) + .selectAll('partners') + .select(withSharedBy) + .select(withSharedWith) + .where('sharedWithId', '=', sharedWithId) + .where('sharedById', '=', sharedById) + .executeTakeFirst() as unknown as Promise; } - create({ sharedById, sharedWithId }: PartnerIds): Promise { - return this.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) + create(values: Insertable): Promise { + return this.db + .insertInto('partners') + .values(values) + .returningAll() + .returning(withSharedBy) + .returning(withSharedWith) + .executeTakeFirstOrThrow() as unknown as Promise; } - async remove(entity: PartnerEntity): Promise { - await this.repository.remove(entity); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }, { inTimeline: true }] }) + update({ sharedWithId, sharedById }: PartnerIds, values: Updateable): Promise { + return this.db + .updateTable('partners') + .set(values) + .where('sharedWithId', '=', sharedWithId) + .where('sharedById', '=', sharedById) + .returningAll() + .returning(withSharedBy) + .returning(withSharedWith) + .executeTakeFirstOrThrow() as unknown as Promise; } - update(entity: Partial): Promise { - return this.save(entity); - } - - private async save(entity: DeepPartial): Promise { - await this.repository.save(entity); - return this.repository.findOneOrFail({ - where: { sharedById: entity.sharedById, sharedWithId: entity.sharedWithId }, - }); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) + async remove({ sharedWithId, sharedById }: PartnerIds): Promise { + await this.db + .deleteFrom('partners') + .where('sharedWithId', '=', sharedWithId) + .where('sharedById', '=', sharedById) + .execute(); } } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index 9e9f09c442..2887fbeb96 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; import { StackEntity } from 'src/entities/stack.entity'; import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface'; @@ -12,6 +13,7 @@ export class StackRepository implements IStackRepository { @InjectRepository(StackEntity) private repository: Repository, ) {} + @GenerateSql({ params: [{ ownerId: DummyValue.UUID }] }) search(query: StackSearch): Promise { return this.repository.find({ where: { @@ -80,6 +82,7 @@ export class StackRepository implements IStackRepository { }); } + @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { const stack = await this.getById(id); if (!stack) { @@ -119,6 +122,7 @@ export class StackRepository implements IStackRepository { return this.save(entity); } + @GenerateSql({ params: [DummyValue.UUID] }) async getById(id: string): Promise { return this.repository.findOne({ where: { diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index f9a8e6ce47..4ee656abe5 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { ReactionType } from 'src/dtos/activity.dto'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { ActivityService } from 'src/services/activity.service'; +import { IActivityRepository } from 'src/types'; import { activityStub } from 'test/fixtures/activity.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index ea7f8b5c0a..feb1074fb2 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -5,15 +5,15 @@ import { ActivityResponseDto, ActivitySearchDto, ActivityStatisticsResponseDto, + mapActivity, MaybeDuplicate, ReactionLevel, ReactionType, - mapActivity, } from 'src/dtos/activity.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ActivityEntity } from 'src/entities/activity.entity'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { ActivityItem } from 'src/types'; @Injectable() export class ActivityService extends BaseService { @@ -43,7 +43,7 @@ export class ActivityService extends BaseService { albumId: dto.albumId, }; - let activity: ActivityEntity | null = null; + let activity: ActivityItem | undefined; let duplicate = false; if (dto.type === ReactionType.LIKE) { diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index bb1aac8e6e..ca6b56e085 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -323,18 +323,16 @@ describe(AlbumService.name, () => { albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(userStub.user2); albumUserMock.create.mockResolvedValue({ - userId: userStub.user2.id, - user: userStub.user2, - albumId: albumStub.sharedWithAdmin.id, - album: albumStub.sharedWithAdmin, + usersId: userStub.user2.id, + albumsId: albumStub.sharedWithAdmin.id, role: AlbumUserRole.EDITOR, }); await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: authStub.user2.user.id }], }); expect(albumUserMock.create).toHaveBeenCalledWith({ - userId: authStub.user2.user.id, - albumId: albumStub.sharedWithAdmin.id, + usersId: authStub.user2.user.id, + albumsId: albumStub.sharedWithAdmin.id, }); expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.sharedWithAdmin.id, @@ -361,8 +359,8 @@ describe(AlbumService.name, () => { expect(albumUserMock.delete).toHaveBeenCalledTimes(1); expect(albumUserMock.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: userStub.user1.id, + albumsId: albumStub.sharedWithUser.id, + usersId: userStub.user1.id, }); expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false }); }); @@ -388,8 +386,8 @@ describe(AlbumService.name, () => { expect(albumUserMock.delete).toHaveBeenCalledTimes(1); expect(albumUserMock.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: authStub.user1.user.id, + albumsId: albumStub.sharedWithUser.id, + usersId: authStub.user1.user.id, }); }); @@ -400,8 +398,8 @@ describe(AlbumService.name, () => { expect(albumUserMock.delete).toHaveBeenCalledTimes(1); expect(albumUserMock.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: authStub.user1.user.id, + albumsId: albumStub.sharedWithUser.id, + usersId: authStub.user1.user.id, }); }); @@ -433,7 +431,7 @@ describe(AlbumService.name, () => { role: AlbumUserRole.EDITOR, }); expect(albumUserMock.update).toHaveBeenCalledWith( - { albumId: albumStub.sharedWithAdmin.id, userId: userStub.admin.id }, + { albumsId: albumStub.sharedWithAdmin.id, usersId: userStub.admin.id }, { role: AlbumUserRole.EDITOR }, ); }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index e57e6b168c..f5685f84eb 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -229,7 +229,7 @@ export class AlbumService extends BaseService { throw new BadRequestException('User not found'); } - await this.albumUserRepository.create({ userId, albumId: id, role }); + await this.albumUserRepository.create({ usersId: userId, albumsId: id, role }); await this.eventRepository.emit('album.invite', { id, userId }); } @@ -257,12 +257,12 @@ export class AlbumService extends BaseService { await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); } - await this.albumUserRepository.delete({ albumId: id, userId }); + await this.albumUserRepository.delete({ albumsId: id, usersId: userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial): Promise { await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); - await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); + await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role }); } private async findOrFail(id: string, options: AlbumInfoOptions) { diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 82852c27e2..9e024daacd 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -7,7 +7,6 @@ import { StorageCore } from 'src/cores/storage.core'; import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; @@ -45,6 +44,7 @@ import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { IViewRepository } from 'src/interfaces/view.interface'; +import { ActivityRepository } from 'src/repositories/activity.repository'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; @@ -54,7 +54,7 @@ export class BaseService { constructor( @Inject(ILoggerRepository) protected logger: ILoggerRepository, @Inject(IAccessRepository) protected accessRepository: IAccessRepository, - @Inject(IActivityRepository) protected activityRepository: IActivityRepository, + protected activityRepository: ActivityRepository, @Inject(IAuditRepository) protected auditRepository: IAuditRepository, @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository, @Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository, diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index a8ca795535..a08cb108a5 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -87,7 +87,7 @@ describe(LibraryService.name, () => { Promise.resolve( [libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find( (library) => library.id === id, - ) || null, + ), ), ); @@ -190,8 +190,6 @@ describe(LibraryService.name, () => { }); it("should fail when library can't be found", async () => { - libraryMock.get.mockResolvedValue(null); - await expect(sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED); }); @@ -242,8 +240,6 @@ describe(LibraryService.name, () => { }); it("should fail when library can't be found", async () => { - libraryMock.get.mockResolvedValue(null); - await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED); }); }); @@ -630,7 +626,6 @@ describe(LibraryService.name, () => { }); it('should throw an error when a library is not found', async () => { - libraryMock.get.mockResolvedValue(null); await expect(sut.get(libraryStub.externalLibrary1.id)).rejects.toBeInstanceOf(BadRequestException); expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.externalLibrary1.id); }); @@ -825,7 +820,10 @@ describe(LibraryService.name, () => { await expect(sut.update('library-id', { importPaths: [`${cwd}/foo/bar`] })).resolves.toEqual( mapLibrary(libraryStub.externalLibrary1), ); - expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' })); + expect(libraryMock.update).toHaveBeenCalledWith( + 'library-id', + expect.objectContaining({ importPaths: [`${cwd}/foo/bar`] }), + ); }); }); @@ -1015,7 +1013,7 @@ describe(LibraryService.name, () => { Promise.resolve( [libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find( (library) => library.id === id, - ) || null, + ), ), ); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 0290b2e7fe..dca1dec9e2 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -311,7 +311,7 @@ export class LibraryService extends BaseService { } } - const library = await this.libraryRepository.update({ id, ...dto }); + const library = await this.libraryRepository.update(id, dto); return mapLibrary(library); } @@ -571,7 +571,7 @@ export class LibraryService extends BaseService { this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`); } - await this.libraryRepository.update({ id: job.id, refreshedAt: new Date() }); + await this.libraryRepository.update(job.id, { refreshedAt: new Date() }); return JobStatus.SUCCESS; } diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index b5dd4c2553..9c5336eb6e 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -69,7 +69,17 @@ describe(MemoryService.name, () => { memoryAt: new Date(2024), }), ).resolves.toMatchObject({ assets: [] }); - expect(memoryMock.create).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); + expect(memoryMock.create).toHaveBeenCalledWith( + { + ownerId: 'admin_id', + memoryAt: expect.any(Date), + type: MemoryType.ON_THIS_DAY, + isSaved: undefined, + sendAt: undefined, + data: { year: 2024 }, + }, + new Set(), + ); }); it('should create a memory', async () => { @@ -80,14 +90,14 @@ describe(MemoryService.name, () => { type: MemoryType.ON_THIS_DAY, data: { year: 2024 }, assetIds: ['asset1'], - memoryAt: new Date(2024), + memoryAt: new Date(2024, 0, 1), }), ).resolves.toBeDefined(); expect(memoryMock.create).toHaveBeenCalledWith( expect.objectContaining({ ownerId: userStub.admin.id, - assets: [{ id: 'asset1' }], }), + new Set(['asset1']), ); }); @@ -115,12 +125,7 @@ describe(MemoryService.name, () => { accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); memoryMock.update.mockResolvedValue(memoryStub.memory1); await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined(); - expect(memoryMock.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'memory1', - isSaved: true, - }), - ); + expect(memoryMock.update).toHaveBeenCalledWith('memory1', expect.objectContaining({ isSaved: true })); }); }); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 816b0fddeb..926571e43c 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @@ -29,15 +28,17 @@ export class MemoryService extends BaseService { permission: Permission.ASSET_SHARE, ids: assetIds, }); - const memory = await this.memoryRepository.create({ - ownerId: auth.user.id, - type: dto.type, - data: dto.data, - isSaved: dto.isSaved, - memoryAt: dto.memoryAt, - seenAt: dto.seenAt, - assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity), - }); + const memory = await this.memoryRepository.create( + { + ownerId: auth.user.id, + type: dto.type, + data: dto.data, + isSaved: dto.isSaved, + memoryAt: dto.memoryAt, + seenAt: dto.seenAt, + }, + allowedAssetIds, + ); return mapMemory(memory); } @@ -45,8 +46,7 @@ export class MemoryService extends BaseService { async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const memory = await this.memoryRepository.update({ - id, + const memory = await this.memoryRepository.update(id, { isSaved: dto.isSaved, memoryAt: dto.memoryAt, seenAt: dto.seenAt, @@ -68,7 +68,7 @@ export class MemoryService extends BaseService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.memoryRepository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update(id, { updatedAt: new Date() }); } return results; @@ -86,7 +86,7 @@ export class MemoryService extends BaseService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.memoryRepository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update(id, { id, updatedAt: new Date() }); } return results; diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index 2e11c4f9ad..e7b7348e98 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -37,7 +37,7 @@ describe(PartnerService.name, () => { describe('create', () => { it('should create a new partner', async () => { - partnerMock.get.mockResolvedValue(null); + partnerMock.get.mockResolvedValue(void 0); partnerMock.create.mockResolvedValue(partnerStub.adminToUser1); await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toBeDefined(); @@ -67,7 +67,7 @@ describe(PartnerService.name, () => { }); it('should throw an error when the partner does not exist', async () => { - partnerMock.get.mockResolvedValue(null); + partnerMock.get.mockResolvedValue(void 0); await expect(sut.remove(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException); @@ -87,11 +87,10 @@ describe(PartnerService.name, () => { partnerMock.update.mockResolvedValue(partnerStub.adminToUser1); await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: true })).resolves.toBeDefined(); - expect(partnerMock.update).toHaveBeenCalledWith({ - sharedById: 'shared-by-id', - sharedWithId: authStub.admin.user.id, - inTimeline: true, - }); + expect(partnerMock.update).toHaveBeenCalledWith( + { sharedById: 'shared-by-id', sharedWithId: authStub.admin.user.id }, + { inTimeline: true }, + ); }); }); }); diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index ee36f1ce45..f17bab24ba 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -43,7 +43,7 @@ export class PartnerService extends BaseService { await this.requireAccess({ auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; - const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline }); + const entity = await this.partnerRepository.update(partnerId, { inTimeline: dto.inTimeline }); return this.mapPartner(entity, PartnerDirection.SharedWith); } diff --git a/server/src/types.ts b/server/src/types.ts index c55de4160d..0d3b037f9e 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,5 +1,6 @@ import { UserEntity } from 'src/entities/user.entity'; import { Permission } from 'src/enum'; +import { ActivityRepository } from 'src/repositories/activity.repository'; export type AuthApiKey = { id: string; @@ -7,3 +8,11 @@ export type AuthApiKey = { user: UserEntity; permissions: Permission[]; }; + +export type RepositoryInterface = Pick; + +export type IActivityRepository = RepositoryInterface; + +export type ActivityItem = + | Awaited> + | Awaited>[0]; diff --git a/server/test/fixtures/activity.stub.ts b/server/test/fixtures/activity.stub.ts index 4805f6604d..9578bcd4a1 100644 --- a/server/test/fixtures/activity.stub.ts +++ b/server/test/fixtures/activity.stub.ts @@ -1,33 +1,39 @@ -import { ActivityEntity } from 'src/entities/activity.entity'; +import { ActivityItem } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; -import { userStub } from 'test/fixtures/user.stub'; export const activityStub = { - oneComment: Object.freeze({ + oneComment: Object.freeze({ id: 'activity-1', comment: 'comment', isLiked: false, - userId: authStub.admin.user.id, - user: userStub.admin, + userId: 'admin_id', + user: { + id: 'admin_id', + name: 'admin', + email: 'admin@test.com', + profileImagePath: '', + profileChangedAt: new Date('2021-01-01'), + }, assetId: assetStub.image.id, - asset: assetStub.image, albumId: albumStub.oneAsset.id, - album: albumStub.oneAsset, createdAt: new Date(), updatedAt: new Date(), }), - liked: Object.freeze({ + liked: Object.freeze({ id: 'activity-2', comment: null, isLiked: true, - userId: authStub.admin.user.id, - user: userStub.admin, + userId: 'admin_id', + user: { + id: 'admin_id', + name: 'admin', + email: 'admin@test.com', + profileImagePath: '', + profileChangedAt: new Date('2021-01-01'), + }, assetId: assetStub.image.id, - asset: assetStub.image, albumId: albumStub.oneAsset.id, - album: albumStub.oneAsset, createdAt: new Date(), updatedAt: new Date(), }), diff --git a/server/test/repositories/activity.repository.mock.ts b/server/test/repositories/activity.repository.mock.ts index 9d29d90ab8..bcc27774e3 100644 --- a/server/test/repositories/activity.repository.mock.ts +++ b/server/test/repositories/activity.repository.mock.ts @@ -1,4 +1,4 @@ -import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { IActivityRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newActivityRepositoryMock = (): Mocked => { diff --git a/server/test/utils.ts b/server/test/utils.ts index 7f5b75020c..bc0ada3259 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -3,7 +3,9 @@ import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import { ImmichWorker } from 'src/enum'; import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { ActivityRepository } from 'src/repositories/activity.repository'; import { BaseService } from 'src/services/base.service'; +import { IActivityRepository } from 'src/types'; import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; @@ -104,7 +106,7 @@ export const newTestService = ( const sut = new Service( loggerMock, accessMock, - activityMock, + activityMock as IActivityRepository as ActivityRepository, auditMock, albumMock, albumUserMock, diff --git a/web/Dockerfile b/web/Dockerfile index dfef1d8348..0b51000426 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f +FROM node:22.13.0-alpine3.20@sha256:db8dcb90326a0116375414e9a7c068a6b87a4422b7da37b5c6cd026f7c7835d3 RUN apk add --no-cache tini USER node diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index f3cf9d7f10..fc5e35ce6d 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -81,6 +81,7 @@ export default [ 'unicorn/prevent-abbreviations': 'off', 'unicorn/no-nested-ternary': 'off', 'unicorn/consistent-function-scoping': 'off', + 'unicorn/filename-case': 'off', 'unicorn/prefer-top-level-await': 'off', 'unicorn/import-style': 'off', 'svelte/button-has-type': 'error', diff --git a/web/package-lock.json b/web/package-lock.json index 426df0acd7..c7fa97e55d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/ui": "^0.13.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -104,7 +105,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "engines": { "node": ">=10" }, @@ -793,6 +793,31 @@ "npm": ">=9.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@formatjs/ecma402-abstract": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", @@ -1279,11 +1304,34 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@immich/ui": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.13.0.tgz", + "integrity": "sha512-kO6uDbO+UpwRdzDI4FSyXkB7UXNDcnMo86gyLfdjZj6on9fy5Eam9KpJlt/zvVDNAqyGQzrBmdQSQl6n+S1JuA==", + "license": "GNU Affero General Public License version 3", + "dependencies": { + "@mdi/js": "^7.4.47", + "bits-ui": "^1.0.0-next.46", + "tailwind-merge": "^2.5.4", + "tailwind-variants": "^0.3.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/@internationalized/date": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz", + "integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1300,7 +1348,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -1312,7 +1359,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -1323,14 +1369,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1347,7 +1391,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1362,7 +1405,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1542,7 +1584,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1555,7 +1596,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -1564,7 +1604,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1605,7 +1644,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -2017,6 +2055,15 @@ "vite": "^5.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.2.0.tgz", @@ -2826,7 +2873,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2846,14 +2892,12 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2865,8 +2909,7 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, "node_modules/argparse": { "version": "2.0.1", @@ -2967,18 +3010,40 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/bits-ui": { + "version": "1.0.0-next.77", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.77.tgz", + "integrity": "sha512-IV0AyVEvsRkXv4s/fl4iea5E9W2b9EBf98s9mRMKMc1xHxM9MmtM2r6MZMqftHQ/c+gHTIt3A9EKuTlh7uay8w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.4", + "@floating-ui/dom": "^1.6.7", + "@internationalized/date": "^3.5.6", + "esm-env": "^1.1.2", + "runed": "^0.22.0", + "svelte-toolbelt": "^0.7.0" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "svelte": "^5.11.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2993,7 +3058,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3093,7 +3157,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "engines": { "node": ">= 6" } @@ -3164,7 +3227,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3189,7 +3251,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3350,7 +3411,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "engines": { "node": ">= 6" } @@ -3388,7 +3448,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3416,7 +3475,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -3580,14 +3638,12 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "node_modules/dom-accessibility-api": { "version": "0.5.16", @@ -3621,8 +3677,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { "version": "1.5.74", @@ -3634,8 +3689,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/engine.io-client": { "version": "6.5.4", @@ -4146,9 +4200,9 @@ } }, "node_modules/esm-env": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", - "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "license": "MIT" }, "node_modules/esniff": { @@ -4306,7 +4360,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4323,7 +4376,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4347,7 +4399,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -4374,7 +4425,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4424,7 +4474,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -4469,7 +4518,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -4482,8 +4530,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/geojson-vt": { "version": "3.2.1", @@ -4527,7 +4574,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4548,7 +4594,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -4560,7 +4605,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4570,7 +4614,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4666,7 +4709,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4852,6 +4894,12 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -4882,7 +4930,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -4909,7 +4956,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -4944,7 +4990,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4953,7 +4998,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -4962,7 +5006,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4974,7 +5017,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5113,7 +5155,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -5128,7 +5169,6 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -5303,8 +5343,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-character": { "version": "3.0.0", @@ -5546,7 +5585,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -5555,7 +5593,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5623,7 +5660,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -5660,7 +5696,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -5671,7 +5706,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -5734,7 +5768,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5760,7 +5793,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5769,7 +5801,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "engines": { "node": ">= 6" } @@ -5850,8 +5881,7 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -5910,7 +5940,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5918,14 +5947,12 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -5940,8 +5967,7 @@ "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/pathe": { "version": "1.1.2", @@ -5976,14 +6002,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -5995,7 +6019,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6004,7 +6027,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "engines": { "node": ">= 6" } @@ -6031,7 +6053,6 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.0.tgz", "integrity": "sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6060,7 +6081,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -6077,7 +6097,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -6125,7 +6144,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6194,7 +6212,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6207,8 +6224,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/potpack": { "version": "2.0.0", @@ -6341,7 +6357,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -6372,7 +6387,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -6483,7 +6497,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -6561,7 +6574,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -6587,7 +6599,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6697,7 +6708,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -6716,6 +6726,21 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/runed": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.22.0.tgz", + "integrity": "sha512-ZWVXWhOr0P5xdNgtviz6D1ivLUDWKLCbeC5SUEJ3zBkqLReVqWHenFxMNFeFaiC5bfxhFxyxzyzB+98uYFtwdA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -6843,7 +6868,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6855,7 +6879,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -6870,7 +6893,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -6979,7 +7001,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7078,7 +7099,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7093,7 +7113,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7107,7 +7126,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7120,7 +7138,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7152,11 +7169,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -7199,7 +7224,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7841,6 +7865,41 @@ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, + "node_modules/svelte-toolbelt": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.0.tgz", + "integrity": "sha512-i/Tv4NwAWWqJnK5H0F8y/ubDnogDYlwwyzKhrspTUFzrFuGnYshqd2g4/R43ds841wmaFiSW/HsdsdWhPOlrAA==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.20.0", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.20.0.tgz", + "integrity": "sha512-YqPxaUdWL5nUXuSF+/v8a+NkVN8TGyEGbQwTA25fLY35MR/2bvZ1c6sCbudoo1kT4CAJPh4kUkcgGVxW127WKw==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/svelte/node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -7858,11 +7917,36 @@ "optional": true, "peer": true }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.0.tgz", + "integrity": "sha512-ho2k5kn+LB1fT5XdNS3Clb96zieWxbStE9wNLK7D0AV64kdZMaYzAKo0fWl6fXLPY99ffF9oBJnIj5escEl/8A==", + "license": "MIT", + "dependencies": { + "tailwind-merge": "^2.5.4" + }, + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwindcss": "*" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -7900,7 +7984,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -7913,7 +7996,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7949,7 +8031,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -8000,7 +8081,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -8009,7 +8089,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -8098,7 +8177,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8163,8 +8241,7 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tslib": { "version": "2.8.1", @@ -8308,8 +8385,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -8581,7 +8657,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -8635,7 +8710,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8652,7 +8726,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -8667,7 +8740,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -8678,8 +8750,7 @@ "node_modules/wrap-ansi-cjs/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", diff --git a/web/package.json b/web/package.json index b843f6c13a..96e8516026 100644 --- a/web/package.json +++ b/web/package.json @@ -67,6 +67,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/ui": "^0.13.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/app.css b/web/src/app.css index d1af865bca..7a547d3504 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -22,6 +22,30 @@ --immich-dark-success: 56 142 60; --immich-dark-warning: 245 124 0; } + + :root { + /* light */ + --immich-ui-primary: 66 80 175; + --immich-ui-dark: 58 58 58; + --immich-ui-light: 255 255 255; + --immich-ui-success: 34 197 94; + --immich-ui-danger: 180 0 0; + --immich-ui-warning: 255 170 0; + --immich-ui-info: 14 165 233; + --immich-ui-default-border: 209 213 219; + } + + .dark { + /* dark */ + --immich-ui-primary: 172 203 250; + --immich-ui-light: 0 0 0; + --immich-ui-dark: 229 231 235; + /* --immich-success: 56 142 60; */ + --immich-ui-danger: 239 68 68; + --immich-ui-warning: 255 170 0; + --immich-ui-info: 14 165 233; + --immich-ui-default-border: 55 65 81; + } } @font-face { diff --git a/web/src/app.html b/web/src/app.html index 6fd02dc9f8..c0ac3cfe6c 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -101,6 +101,12 @@ + +
diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte index 85a7260f40..12b83a2b4c 100644 --- a/web/src/lib/components/album-page/albums-controls.svelte +++ b/web/src/lib/components/album-page/albums-controls.svelte @@ -1,15 +1,30 @@
- createAlbumAndRedirect()}> -
- - -
-
+ {/if} - handleChangeListMode()}> -
- {#if $albumViewSettings.view === AlbumViewMode.List} - - - {:else} - - - {/if} -
-
+ diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 7a2f97bb65..ea5d6e9275 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -8,7 +8,6 @@ import { updateNumberOfComments } from '$lib/stores/activity.store'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { AssetStore } from '$lib/stores/assets.store'; import { isShowDetail } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; @@ -49,8 +48,9 @@ import VideoViewer from './video-wrapper-viewer.svelte'; import ImagePanoramaViewer from './image-panorama-viewer.svelte'; + type HasAsset = boolean; + interface Props { - assetStore?: AssetStore | null; asset: AssetResponseDto; preloadAssets?: AssetResponseDto[]; showNavigation?: boolean; @@ -61,13 +61,13 @@ onAction?: OnAction | undefined; reactions?: ActivityResponseDto[]; onClose: (dto: { asset: AssetResponseDto }) => void; - onNext: () => void; - onPrevious: () => void; + onNext: () => Promise; + onPrevious: () => Promise; + onRandom: () => Promise; copyImage?: () => Promise; } let { - assetStore = null, asset = $bindable(), preloadAssets = $bindable([]), showNavigation = true, @@ -80,6 +80,7 @@ onClose, onNext, onPrevious, + onRandom, copyImage = $bindable(), }: Props = $props(); @@ -271,22 +272,6 @@ }); }; - const navigateAssetRandom = async () => { - if (!assetStore) { - return; - } - - const asset = await assetStore.getRandomAsset(); - if (!asset) { - return; - } - - slideshowHistory.queue(asset); - - setAsset(asset); - $restartSlideshowProgress = true; - }; - const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => { if (!order) { if ($slideshowState === SlideshowState.PlaySlideshow) { @@ -296,23 +281,30 @@ } } + e?.stopPropagation(); + + let hasNext = false; + if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { - return (order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next()) || navigateAssetRandom(); + hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); + if (!hasNext) { + const asset = await onRandom(); + if (asset) { + slideshowHistory.queue(asset); + hasNext = true; + } + } + } else { + hasNext = order === 'previous' ? await onPrevious() : await onNext(); } - if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) { - const hasNext = - order === 'previous' ? await assetStore.getPreviousAsset(asset) : await assetStore.getNextAsset(asset); + if ($slideshowState === SlideshowState.PlaySlideshow) { if (hasNext) { $restartSlideshowProgress = true; } else { await handleStopSlideshow(); } } - - e?.stopPropagation(); - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - order === 'previous' ? onPrevious() : onNext(); }; // const showEditorHandler = () => { @@ -435,7 +427,7 @@ {person} {stack} showDetailButton={enableDetailPanel} - showSlideshow={!!assetStore} + showSlideshow={true} onZoomImage={zoomToggle} onCopyImage={copyImage} onAction={handleAction} diff --git a/web/src/lib/components/elements/buttons/link-button.svelte b/web/src/lib/components/elements/buttons/link-button.svelte deleted file mode 100644 index a39e2608cf..0000000000 --- a/web/src/lib/components/elements/buttons/link-button.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index b146f347dc..25d279b0f4 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/components/elements/dropdown.svelte @@ -11,14 +11,12 @@ - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- - {#if errorMessage} -

{errorMessage}

- {/if} - -
- -
-
diff --git a/web/src/lib/components/forms/change-password-form.svelte b/web/src/lib/components/forms/change-password-form.svelte deleted file mode 100644 index 6f16781d9a..0000000000 --- a/web/src/lib/components/forms/change-password-form.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -
-
- - -
- -
- - -
- - {#if errorMessage} -

{errorMessage}

- {/if} - -
- -
-
diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 7aa1c76ed3..8bd955734a 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -5,10 +5,8 @@ import { ByteUnit, convertToBytes } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { createUserAdmin } from '@immich/sdk'; + import { Alert, Button, Field, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui'; import { t } from 'svelte-i18n'; - import Button from '../elements/buttons/button.svelte'; - import Slider from '../elements/slider.svelte'; - import PasswordField from '../shared-components/password-field.svelte'; interface Props { onClose: () => void; @@ -17,137 +15,114 @@ oauthEnabled?: boolean; } - let { onClose, onSubmit, onCancel, oauthEnabled = false }: Props = $props(); + let { onClose, onSubmit: onDone, onCancel, oauthEnabled = false }: Props = $props(); let error = $state(''); - let success = $state(''); + let success = $state(false); let email = $state(''); let password = $state(''); - let confirmPassword = $state(''); + let passwordConfirm = $state(''); let name = $state(''); let shouldChangePassword = $state(true); let notify = $state(true); - let canCreateUser = $state(false); - let quotaSize: number | undefined = $state(); + let quotaSize: string | undefined = $state(); let isCreatingUser = $state(false); - let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null); + let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null); let quotaSizeWarning = $derived( quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw, ); - $effect(() => { - if (password !== confirmPassword && confirmPassword.length > 0) { - error = $t('password_does_not_match'); - canCreateUser = false; - } else { - error = ''; - canCreateUser = true; - } - }); + const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0); + const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : ''); + const valid = $derived(!passwordMismatch && !isCreatingUser); - async function registerUser() { - if (canCreateUser && !isCreatingUser) { - isCreatingUser = true; - error = ''; - - try { - await createUserAdmin({ - userAdminCreateDto: { - email, - password, - shouldChangePassword, - name, - quotaSizeInBytes, - notify, - }, - }); - - success = $t('new_user_created'); - - onSubmit(); - - return; - } catch (error) { - handleError(error, $t('errors.unable_to_create_user')); - } finally { - isCreatingUser = false; - } - } - } - - const onsubmit = async (event: Event) => { + const onSubmit = async (event: Event) => { event.preventDefault(); - await registerUser(); + + if (!valid) { + return; + } + + isCreatingUser = true; + error = ''; + + try { + await createUserAdmin({ + userAdminCreateDto: { + email, + password, + shouldChangePassword, + name, + quotaSizeInBytes, + notify, + }, + }); + + success = true; + + onDone(); + + return; + } catch (error) { + handleError(error, $t('errors.unable_to_create_user')); + } finally { + isCreatingUser = false; + } }; - -
-
- - -
- - {#if $featureFlags.email} -
- - -
- {/if} - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- + + {#if error} -

{error}

+ {/if} {#if success} -

{success}

+

{$t('new_user_created')}

{/if} - - {#snippet stickyBottom()} - - - {/snippet} -
+ + + + + + {#if $featureFlags.email} + + + + {/if} + + + + + + + + {passwordMismatchMessage} + + + + + + + + + + + + + {#if quotaSizeWarning} + {$t('errors.quota_higher_than_disk_size')} + {/if} + + + + {#snippet stickyBottom()} + + + {/snippet} +
+ diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte deleted file mode 100644 index 6c1dcecba3..0000000000 --- a/web/src/lib/components/forms/login-form.svelte +++ /dev/null @@ -1,177 +0,0 @@ - - -{#if !oauthLoading && $featureFlags.passwordLogin} -
- {#if errorMessage} -

- {errorMessage} -

- {/if} - -
- - -
- -
- - -
- -
- -
-
-{/if} - -{#if $featureFlags.oauth} - {#if $featureFlags.passwordLogin} -
-
- - {$t('or')} - -
- {/if} -
- {#if oauthError} -

{oauthError}

- {/if} - -
-{/if} - -{#if !$featureFlags.passwordLogin && !$featureFlags.oauth} -

{$t('login_has_been_disabled')}

-{/if} diff --git a/web/src/lib/components/layouts/AuthPageLayout.svelte b/web/src/lib/components/layouts/AuthPageLayout.svelte new file mode 100644 index 0000000000..78ff67cfcb --- /dev/null +++ b/web/src/lib/components/layouts/AuthPageLayout.svelte @@ -0,0 +1,25 @@ + + +
+ + + + + {title} + + + + {@render children?.()} + + +
diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte index 270978e120..387da7f997 100644 --- a/web/src/lib/components/map-page/map-settings-modal.svelte +++ b/web/src/lib/components/map-page/map-settings-modal.svelte @@ -1,13 +1,11 @@ - -
- - - - - - {#if customDateRange} -
-
- - -
-
- - -
-
- { - customDateRange = false; - settings.dateAfter = ''; - settings.dateBefore = ''; - }} - > - {$t('remove_custom_date_range')} - -
-
- {:else} -
- -
- { - customDateRange = true; - settings.relativeDate = ''; - }} - > - {$t('use_custom_date_range')} - -
-
- {/if} - +
+ + + + + + + + + + + + + + + + + - {#snippet stickyBottom()} - - - {/snippet} - + {#if customDateRange} +
+
+ + +
+
+ + +
+
+ +
+
+ {:else} +
+ +
+ +
+
+ {/if} + + + {#snippet stickyBottom()} + + + {/snippet} + +
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 55f935c8dd..fd98f7e6a3 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -527,6 +527,18 @@ return !!nextAsset; }; + const handleRandom = async () => { + const randomAsset = await $assetStore.getRandomAsset(); + + if (randomAsset) { + const preloadAsset = await $assetStore.getNextAsset(randomAsset); + assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []); + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + } + + return randomAsset; + }; + const handleClose = async ({ asset }: { asset: AssetResponseDto }) => { assetViewingStore.showAssetViewer(false); showSkeleton = true; @@ -911,7 +923,6 @@ {#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} {/await} diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index aff583d1fc..b4bf9043be 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -65,9 +65,9 @@ aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} bind:this={menuElement} - class:max-h-[100vh]={isVisible} - class:max-h-0={!isVisible} - class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none" + class="{isVisible + ? 'max-h-screen max-h-svh' + : 'max-h-0'} flex flex-col transition-all duration-[250ms] ease-in-out outline-none overflow-auto" role="menu" tabindex="-1" > diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 443e8f06b1..77890529b7 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -1,21 +1,20 @@ - -
-
-
- -

- {title} -

-
- - {#if showMessage} -
- {@render message?.()} -
- {/if} - - {@render children?.()} -
-
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 65c6c20e7b..4c3c35aeca 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -34,6 +34,7 @@ isShowDeleteConfirmation?: boolean; onPrevious?: (() => Promise) | undefined; onNext?: (() => Promise) | undefined; + onRandom?: (() => Promise) | undefined; } let { @@ -47,6 +48,7 @@ isShowDeleteConfirmation = $bindable(false), onPrevious = undefined, onNext = undefined, + onRandom = undefined, }: Props = $props(); let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; @@ -202,35 +204,71 @@ })(), ); - const handleNext = async () => { + const handleNext = async (): Promise => { try { let asset: AssetResponseDto | undefined; if (onNext) { asset = await onNext(); } else { - currentViewAssetIndex = Math.min(currentViewAssetIndex + 1, assets.length - 1); - asset = assets[currentViewAssetIndex]; + currentViewAssetIndex = currentViewAssetIndex + 1; + asset = currentViewAssetIndex < assets.length ? assets[currentViewAssetIndex] : undefined; + } + + if (!asset) { + return false; } await navigateToAsset(asset); + return true; } catch (error) { handleError(error, $t('errors.cannot_navigate_next_asset')); + return false; } }; - const handlePrevious = async () => { + const handleRandom = async (): Promise => { + try { + let asset: AssetResponseDto | undefined; + if (onRandom) { + asset = await onRandom(); + } else { + if (assets.length > 0) { + const randomIndex = Math.floor(Math.random() * assets.length); + asset = assets[randomIndex]; + } + } + + if (!asset) { + return null; + } + + await navigateToAsset(asset); + return asset; + } catch (error) { + handleError(error, $t('errors.cannot_navigate_next_asset')); + return null; + } + }; + + const handlePrevious = async (): Promise => { try { let asset: AssetResponseDto | undefined; if (onPrevious) { asset = await onPrevious(); } else { - currentViewAssetIndex = Math.max(currentViewAssetIndex - 1, 0); - asset = assets[currentViewAssetIndex]; + currentViewAssetIndex = currentViewAssetIndex - 1; + asset = currentViewAssetIndex >= 0 ? assets[currentViewAssetIndex] : undefined; + } + + if (!asset) { + return false; } await navigateToAsset(asset); + return true; } catch (error) { handleError(error, $t('errors.cannot_navigate_previous_asset')); + return false; } }; @@ -372,6 +410,7 @@ onAction={handleAction} onPrevious={handlePrevious} onNext={handleNext} + onRandom={handleRandom} onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index ae63a249b5..193cc2ed43 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -2,25 +2,25 @@ import { page } from '$app/state'; import { clickOutside } from '$lib/actions/click-outside'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; import Icon from '$lib/components/elements/icon.svelte'; + import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte'; + import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; + import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; + import { AppRoute } from '$lib/constants'; import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { userInteraction } from '$lib/stores/user.svelte'; import { handleLogout } from '$lib/utils/auth'; import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk'; + import { Button, IconButton } from '@immich/ui'; import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js'; + import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; - import { AppRoute } from '$lib/constants'; - import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; - import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import ThemeButton from '../theme-button.svelte'; import UserAvatar from '../user-avatar.svelte'; import AccountInfoPanel from './account-info-panel.svelte'; - import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte'; - import { onMount } from 'svelte'; interface Props { showUploadButton?: boolean; @@ -87,22 +87,24 @@ onEscape: () => (shouldShowHelpPanel = false), }} > - (shouldShowHelpPanel = !shouldShowHelpPanel)} - padding="1" /> {#if !page.url.pathname.includes('/admin') && showUploadButton} -
{/if} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 79760b192c..5ea5898c2d 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -328,6 +328,11 @@ } }; + const handleDeleteAssets = async (assetIds: string[]) => { + $assetStore.removeAssets(assetIds); + await updateAssetCount(); + }; + onDestroy(() => { assetStore.destroy(); }); @@ -404,7 +409,7 @@ {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} - $assetStore.removeAssets(assetIds)} /> + handleDeleteAssets(assetIds)} /> {:else} diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index 1e59a2720d..33282761aa 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -1,14 +1,11 @@
-
+
- +
-

{$t('welcome_to_immich')}

-
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index b323a136aa..e3657f805a 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -1,6 +1,5 @@ - - {#snippet message()} -

- {$t('hi_user', { values: { name: $user.name, email: $user.email } })} -
-
- {$t('change_password_description')} -

- {/snippet} + +
+ + + {$t('hi_user', { values: { name: $user.name, email: $user.email } })} + {$t('change_password_description')} + + +
- -
+
+ + + + + + + + {errorMessage} + + +
+ +
+
+
+ diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 0ab506f5e3..bc062292fc 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -1,9 +1,14 @@ {#if $featureFlags.loaded} - - {#snippet message()} -

+ + {#if $serverConfig.loginPageMessage} + {@html $serverConfig.loginPageMessage} -

- {/snippet} + + {/if} - await goto(AppRoute.PHOTOS, { invalidateAll: true })} - onFirstLogin={async () => await goto(AppRoute.AUTH_CHANGE_PASSWORD)} - onOnboarding={async () => await goto(AppRoute.AUTH_ONBOARDING)} - /> -
+ {#if !oauthLoading && $featureFlags.passwordLogin} +
+ {#if errorMessage} + + {/if} + + + + + + + + + + + + {/if} + + {#if $featureFlags.oauth} + {#if $featureFlags.passwordLogin} +
+
+ + {$t('or').toUpperCase()} + +
+ {/if} +
+ {#if oauthError} + + {/if} + +
+ {/if} + + {#if !$featureFlags.passwordLogin && !$featureFlags.oauth} + + {/if} + {/if} diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index 2e55ba7435..50551358ea 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -1,22 +1,81 @@ - - {#snippet message()} -

- {$t('admin.registration_description')} -

- {/snippet} + +
+ + {$t('admin.registration_description')} + +
- -
+
+ + + + + + + + + + + + + + + + + {#if errorMessage} + + {/if} + +
+ +
+ + diff --git a/web/tailwind.config.js b/web/tailwind.config.js index eb1ea78fae..2d81c28dd0 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -2,7 +2,11 @@ import plugin from 'tailwindcss/plugin'; /** @type {import('tailwindcss').Config} */ export default { - content: ['./src/**/*.{html,js,svelte,ts}'], + content: [ + './src/**/*.{html,js,svelte,ts}', + './node_modules/@immich/ui/dist/**/*.{svelte,js}', + '../../ui/src/**/*.{html,js,svelte,ts}', + ], darkMode: 'class', theme: { extend: { @@ -24,7 +28,20 @@ export default { 'immich-dark-error': 'rgb(var(--immich-dark-error) / )', 'immich-dark-success': 'rgb(var(--immich-dark-success) / )', 'immich-dark-warning': 'rgb(var(--immich-dark-warning) / )', + + primary: 'rgb(var(--immich-ui-primary) / )', + light: 'rgb(var(--immich-ui-light) / )', + dark: 'rgb(var(--immich-ui-dark) / )', + success: 'rgb(var(--immich-ui-success) / )', + danger: 'rgb(var(--immich-ui-danger) / )', + warning: 'rgb(var(--immich-ui-warning) / )', + info: 'rgb(var(--immich-ui-info) / )', + subtle: 'rgb(var(--immich-gray) / )', }, + borderColor: ({ theme }) => ({ + ...theme('colors'), + DEFAULT: 'rgb(var(--immich-ui-default-border) / )', + }), fontFamily: { 'immich-mono': ['Overpass Mono', 'monospace'], }, diff --git a/web/vite.config.js b/web/vite.config.js index 266312e137..5d134beab0 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -19,6 +19,7 @@ export default defineConfig({ 'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js', // eslint-disable-next-line unicorn/prefer-module '@test-data': path.resolve(__dirname, './src/test-data'), + // '@immich/ui': path.resolve(__dirname, '../../ui'), }, }, server: {