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