Compare commits
5 Commits
feat/sort-
...
feat/corem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57933af9b0 | ||
|
|
1c4a8c3968 | ||
|
|
e2d80755c6 | ||
|
|
5a3b11d603 | ||
|
|
543bc72ae3 |
@@ -23,32 +23,23 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c
|
||||
It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
|
||||
:::
|
||||
|
||||
### Automatic Database Dumps
|
||||
### Automatic Database Backups
|
||||
|
||||
:::warning
|
||||
The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files.
|
||||
There is no monitoring for these dumps and you will not be notified if they are unsuccessful.
|
||||
:::
|
||||
For convenience, Immich will automatically create database backups by default. The backups are stored in `UPLOAD_LOCATION/backups`.
|
||||
As mentioned above, you should make your own backup of these together with the asset folders as noted below.
|
||||
You can adjust the schedule and amount of kept backups in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup).
|
||||
By default, Immich will keep the last 14 backups and create a new backup every day at 2:00 AM.
|
||||
|
||||
:::caution
|
||||
The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below.
|
||||
:::
|
||||
#### Trigger Backup
|
||||
|
||||
For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`.
|
||||
Please be sure to make your own, independent backup of the database together with the asset folders as noted below.
|
||||
You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup).
|
||||
By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM.
|
||||
|
||||
#### Trigger Dump
|
||||
|
||||
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/jobs-status).
|
||||
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
|
||||
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
|
||||
This dumps will count towards the last `X` dumps that will be kept based on your settings.
|
||||
You are able to trigger a backup in the [admin job status page](http://my.immich.app/admin/jobs-status).
|
||||
Visit the page, open the "Create job" modal from the top right, select "Backup Database" and click "Confirm".
|
||||
A job will run and trigger a backup, you can verify this worked correctly by checking the logs or the backup folder.
|
||||
This backup will count towards the last X backups that will be kept based on your settings.
|
||||
|
||||
#### Restoring
|
||||
|
||||
We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host.
|
||||
We hope to make restoring simpler in future versions, for now you can find the backups in the `UPLOAD_LOCATION/backups` folder on your host.
|
||||
Then please follow the steps in the following section for restoring the database.
|
||||
|
||||
### Manual Backup and Restore
|
||||
|
||||
@@ -42,7 +42,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
|
||||
- The GPU must have compute capability 5.2 or greater.
|
||||
- The server must have the official NVIDIA driver installed.
|
||||
- The installed driver must be >= 545 (it must support CUDA 12.3).
|
||||
- The installed driver must be >= 535 (it must support CUDA 12.2).
|
||||
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
|
||||
|
||||
#### ROCm
|
||||
|
||||
@@ -148,30 +148,31 @@ 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`<sup>\*1</sup> | 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`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | 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` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and 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`<sup>\*4</sup> | 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_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
|
||||
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
|
||||
| `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`<sup>\*1</sup> | 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_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and 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`<sup>\*4</sup> | 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_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
|
||||
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
|
||||
| `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.
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ import {
|
||||
mdiWeb,
|
||||
mdiDatabaseOutline,
|
||||
mdiLinkEdit,
|
||||
mdiTagFaces,
|
||||
mdiMovieOpenPlayOutline,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
@@ -84,8 +83,6 @@ import React from 'react';
|
||||
import { Item, Timeline } from '../components/timeline';
|
||||
|
||||
const releases = {
|
||||
'v1.130.0': new Date(2025, 2, 25),
|
||||
'v1.127.0': new Date(2025, 1, 26),
|
||||
'v1.122.0': new Date(2024, 11, 5),
|
||||
'v1.120.0': new Date(2024, 10, 6),
|
||||
'v1.114.0': new Date(2024, 8, 6),
|
||||
@@ -245,21 +242,6 @@ const roadmap: Item[] = [
|
||||
];
|
||||
|
||||
const milestones: Item[] = [
|
||||
withRelease({
|
||||
icon: mdiFolderMultiple,
|
||||
iconColor: 'brown',
|
||||
title: 'Folders view in the mobile app',
|
||||
description: 'Browse your photos and videos in their folder structure inside the mobile app',
|
||||
release: 'v1.130.0',
|
||||
}),
|
||||
withRelease({
|
||||
icon: mdiTagFaces,
|
||||
iconColor: 'teal',
|
||||
title: 'Manual face tagging',
|
||||
description:
|
||||
'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.',
|
||||
release: 'v1.127.0',
|
||||
}),
|
||||
{
|
||||
icon: mdiStar,
|
||||
iconColor: 'gold',
|
||||
@@ -284,8 +266,8 @@ const milestones: Item[] = [
|
||||
withRelease({
|
||||
icon: mdiDatabaseOutline,
|
||||
iconColor: 'brown',
|
||||
title: 'Automatic database dumps',
|
||||
description: 'Database dumps are now integrated into the Immich server',
|
||||
title: 'Automatic database backups',
|
||||
description: 'Database backups are now integrated into the Immich server',
|
||||
release: 'v1.120.0',
|
||||
}),
|
||||
{
|
||||
@@ -318,7 +300,7 @@ const milestones: Item[] = [
|
||||
withRelease({
|
||||
icon: mdiFolderMultiple,
|
||||
iconColor: 'brown',
|
||||
title: 'Folders view',
|
||||
title: 'Folders',
|
||||
description: 'Browse your photos and videos in their folder structure',
|
||||
release: 'v1.113.0',
|
||||
}),
|
||||
|
||||
12
i18n/en.json
12
i18n/en.json
@@ -39,11 +39,11 @@
|
||||
"authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.",
|
||||
"authentication_settings_reenable": "To re-enable, use a <link>Server Command</link>.",
|
||||
"background_task_job": "Background Tasks",
|
||||
"backup_database": "Create Database Dump",
|
||||
"backup_database_enable_description": "Enable database dumps",
|
||||
"backup_keep_last_amount": "Amount of previous dumps to keep",
|
||||
"backup_settings": "Database Dump Settings",
|
||||
"backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.",
|
||||
"backup_database": "Backup Database",
|
||||
"backup_database_enable_description": "Enable database backups",
|
||||
"backup_keep_last_amount": "Amount of previous backups to keep",
|
||||
"backup_settings": "Backup Settings",
|
||||
"backup_settings_description": "Manage database backup settings",
|
||||
"check_all": "Check All",
|
||||
"cleanup": "Cleanup",
|
||||
"cleared_jobs": "Cleared jobs for: {job}",
|
||||
@@ -758,7 +758,6 @@
|
||||
"display_order": "Display order",
|
||||
"display_original_photos": "Display original photos",
|
||||
"display_original_photos_setting_description": "Prefer to display the original photo when viewing an asset rather than thumbnails when the original asset is web-compatible. This may result in slower photo display speeds.",
|
||||
"distance": "Distance",
|
||||
"do_not_show_again": "Do not show this message again",
|
||||
"documentation": "Documentation",
|
||||
"done": "Done",
|
||||
@@ -1711,7 +1710,6 @@
|
||||
"sort_modified": "Date modified",
|
||||
"sort_oldest": "Oldest photo",
|
||||
"sort_people_by_similarity": "Sort people by similarity",
|
||||
"sort_places_by": "Sort places by",
|
||||
"sort_recent": "Most recent photo",
|
||||
"sort_title": "Title",
|
||||
"source": "Source",
|
||||
|
||||
@@ -65,7 +65,8 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:49d73c49616929b0a4f37c50fee0056eb4b0f15de624591e8d9bf84b4dfdd3ce AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:49d73c49616929b0a4f37c50fee0056eb4b0f15de624591e8d9bf84b4dfdd3ce AS prod-openvino
|
||||
|
||||
@@ -82,7 +83,8 @@ RUN apt-get update && \
|
||||
|
||||
FROM nvidia/cuda:12.2.2-runtime-ubuntu22.04@sha256:94c1577b2cd9dd6c0312dc04dff9cb2fdce2b268018abc3d7c2dbcacf1155000 AS prod-cuda
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq libcudnn9-cuda-12 && \
|
||||
@@ -98,7 +100,8 @@ FROM rocm/dev-ubuntu-22.04:6.3.4-complete@sha256:1f7e92ca7e3a3785680473329ed1091
|
||||
FROM prod-cpu AS prod-armnn
|
||||
|
||||
ENV LD_LIBRARY_PATH=/opt/armnn \
|
||||
LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||
LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ocl-icd-libopencl1 mesa-opencl-icd libgomp1 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
@@ -118,7 +121,8 @@ COPY --from=builder-armnn \
|
||||
|
||||
FROM prod-cpu AS prod-rknn
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/v2.3.0/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class Settings(BaseSettings):
|
||||
request_threads: int = os.cpu_count() or 4
|
||||
model_inter_op_threads: int = 0
|
||||
model_intra_op_threads: int = 0
|
||||
model_arena: bool = True
|
||||
ann: bool = True
|
||||
ann_fp16_turbo: bool = False
|
||||
ann_tuning_level: int = 2
|
||||
|
||||
@@ -79,6 +79,7 @@ SUPPORTED_PROVIDERS = [
|
||||
"CUDAExecutionProvider",
|
||||
"ROCMExecutionProvider",
|
||||
"OpenVINOExecutionProvider",
|
||||
"CoreMLExecutionProvider",
|
||||
"CPUExecutionProvider",
|
||||
]
|
||||
|
||||
|
||||
@@ -96,6 +96,14 @@ class OrtSession:
|
||||
"precision": "FP32",
|
||||
"cache_dir": (self.model_path.parent / "openvino").as_posix(),
|
||||
}
|
||||
case "CoreMLExecutionProvider":
|
||||
options = {
|
||||
"ModelFormat": "MLProgram",
|
||||
"MLComputeUnits": "ALL",
|
||||
"SpecializationStrategy": "FastPrediction",
|
||||
"AllowLowPrecisionAccumulationOnGPU": "1",
|
||||
"ModelCacheDirectory": (self.model_path.parent / "coreml").as_posix(),
|
||||
}
|
||||
case _:
|
||||
options = {}
|
||||
provider_options.append(options)
|
||||
@@ -115,7 +123,7 @@ class OrtSession:
|
||||
@property
|
||||
def _sess_options_default(self) -> ort.SessionOptions:
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.enable_cpu_mem_arena = False
|
||||
sess_options.enable_cpu_mem_arena = settings.model_arena
|
||||
|
||||
# avoid thread contention between models
|
||||
if settings.model_inter_op_threads > 0:
|
||||
|
||||
@@ -180,6 +180,7 @@ class TestOrtSession:
|
||||
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
|
||||
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
|
||||
ROCM_EP = ["ROCMExecutionProvider", "CPUExecutionProvider"]
|
||||
COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"]
|
||||
|
||||
@pytest.mark.providers(CPU_EP)
|
||||
def test_sets_cpu_provider(self, providers: list[str]) -> None:
|
||||
@@ -225,6 +226,12 @@ class TestOrtSession:
|
||||
|
||||
assert session.providers == self.ROCM_EP
|
||||
|
||||
@pytest.mark.providers(COREML_EP)
|
||||
def test_uses_coreml(self, providers: list[str]) -> None:
|
||||
session = OrtSession("ViT-B-32__openai")
|
||||
|
||||
assert session.providers == self.COREML_EP
|
||||
|
||||
def test_sets_provider_kwarg(self) -> None:
|
||||
providers = ["CUDAExecutionProvider"]
|
||||
session = OrtSession("ViT-B-32__openai", providers=providers)
|
||||
@@ -284,7 +291,6 @@ class TestOrtSession:
|
||||
assert session.sess_options.execution_mode == ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
assert session.sess_options.inter_op_num_threads == 1
|
||||
assert session.sess_options.intra_op_num_threads == 2
|
||||
assert session.sess_options.enable_cpu_mem_arena is False
|
||||
|
||||
def test_sets_default_sess_options_does_not_set_threads_if_non_cpu_and_default_threads(self) -> None:
|
||||
session = OrtSession("ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
|
||||
@@ -302,6 +308,26 @@ class TestOrtSession:
|
||||
assert session.sess_options.inter_op_num_threads == 2
|
||||
assert session.sess_options.intra_op_num_threads == 4
|
||||
|
||||
def test_uses_arena_if_enabled(self, mocker: MockerFixture) -> None:
|
||||
mock_settings = mocker.patch("immich_ml.sessions.ort.settings", autospec=True)
|
||||
mock_settings.model_inter_op_threads = 0
|
||||
mock_settings.model_intra_op_threads = 0
|
||||
mock_settings.model_arena = True
|
||||
|
||||
session = OrtSession("ViT-B-32__openai", providers=["CPUExecutionProvider"])
|
||||
|
||||
assert session.sess_options.enable_cpu_mem_arena
|
||||
|
||||
def test_does_not_use_arena_if_disabled(self, mocker: MockerFixture) -> None:
|
||||
mock_settings = mocker.patch("immich_ml.sessions.ort.settings", autospec=True)
|
||||
mock_settings.model_inter_op_threads = 0
|
||||
mock_settings.model_intra_op_threads = 0
|
||||
mock_settings.model_arena = False
|
||||
|
||||
session = OrtSession("ViT-B-32__openai", providers=["CPUExecutionProvider"])
|
||||
|
||||
assert not session.sess_options.enable_cpu_mem_arena
|
||||
|
||||
def test_sets_sess_options_kwarg(self) -> None:
|
||||
sess_options = ort.SessionOptions()
|
||||
session = OrtSession(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -45,7 +45,7 @@ post_install do |installer|
|
||||
installer.generated_projects.each do |project|
|
||||
project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -224,7 +224,7 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
background_downloader: b42a56120f5348bff70e74222f0e9e6f7f1a1537
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
@@ -261,6 +261,6 @@ SPEC CHECKSUMS:
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
|
||||
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
|
||||
PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -690,7 +690,7 @@
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -720,7 +720,7 @@
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class PlaceResult {
|
||||
/// The label to show associated with this curated object
|
||||
final String label;
|
||||
|
||||
/// The id to lookup the asset from the server
|
||||
final String id;
|
||||
|
||||
/// The latitude of the location
|
||||
final double latitude;
|
||||
|
||||
/// The longitude of the location
|
||||
final double longitude;
|
||||
|
||||
PlaceResult({
|
||||
required this.label,
|
||||
required this.id,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
});
|
||||
|
||||
PlaceResult copyWith({
|
||||
String? label,
|
||||
String? id,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
}) {
|
||||
return PlaceResult(
|
||||
label: label ?? this.label,
|
||||
id: id ?? this.id,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'label': label,
|
||||
'id': id,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
};
|
||||
}
|
||||
|
||||
factory PlaceResult.fromMap(Map<String, dynamic> map) {
|
||||
return PlaceResult(
|
||||
label: map['label'] as String,
|
||||
id: map['id'] as String,
|
||||
latitude: map['latitude'] as double,
|
||||
longitude: map['longitude'] as double,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory PlaceResult.fromJson(String source) =>
|
||||
PlaceResult.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'CuratedContent(label: $label, id: $id, latitude: $latitude, longitude: $longitude)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant PlaceResult other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.label == label &&
|
||||
other.id == id &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
label.hashCode ^ id.hashCode ^ latitude.hashCode ^ longitude.hashCode;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
@@ -13,7 +12,6 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||
@@ -299,35 +297,33 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
context.colorScheme.primary.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
padding: const EdgeInsets.all(12),
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: albums.take(4).map((album) {
|
||||
return AlbumThumbnailCard(
|
||||
album: album,
|
||||
showTitle: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -357,66 +353,43 @@ class PlacesCollectionCard extends StatelessWidget {
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = context.width * widthFactor - 20.0;
|
||||
|
||||
return FutureBuilder<(Position?, LocationPermission?)>(
|
||||
future: MapUtils.checkPermAndGetLocation(
|
||||
context: context,
|
||||
silent: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
var position = snapshot.data?.$1;
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(
|
||||
PlacesCollectionRoute(
|
||||
currentLocation: position != null
|
||||
? LatLng(position.latitude, position.longitude)
|
||||
: null,
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: size,
|
||||
width: size,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
color: context.colorScheme.secondaryContainer
|
||||
.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: LatLng(
|
||||
position?.latitude ?? 21.44950,
|
||||
position?.longitude ?? -157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode: context.isDarkTheme
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'places'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,28 +13,18 @@ import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/calculate_distance.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
enum FilterType {
|
||||
name,
|
||||
distance,
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class PlacesCollectionPage extends HookConsumerWidget {
|
||||
const PlacesCollectionPage({super.key, this.currentLocation});
|
||||
final LatLng? currentLocation;
|
||||
|
||||
const PlacesCollectionPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final places = ref.watch(getAllPlacesProvider);
|
||||
final formFocus = useFocusNode();
|
||||
final ValueNotifier<String?> search = useState(null);
|
||||
final filterType = useState(FilterType.name);
|
||||
final isAscending = useState(true); // Add state for sort order
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -61,83 +51,25 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
||||
body: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
if (search.value == null) ...[
|
||||
if (search.value == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
width: context.width,
|
||||
child: MapThumbnail(
|
||||
onTap: (_, __) => context
|
||||
.pushRoute(MapRoute(initialLocation: currentLocation)),
|
||||
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
||||
zoom: 8,
|
||||
centre: currentLocation ??
|
||||
const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
centre: const LatLng(
|
||||
21.44950,
|
||||
-157.91959,
|
||||
),
|
||||
showAttribution: false,
|
||||
themeMode:
|
||||
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (currentLocation != null) ...[
|
||||
Text('sort_places_by'.tr()),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButton(
|
||||
value: filterType.value,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: FilterType.name,
|
||||
child: Text('name'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: FilterType.distance,
|
||||
child: Text('distance'.tr()),
|
||||
),
|
||||
],
|
||||
onChanged: (e) {
|
||||
filterType.value = e!;
|
||||
},
|
||||
isExpanded: false,
|
||||
underline: const SizedBox(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.swap_vert,
|
||||
),
|
||||
onPressed: () {
|
||||
isAscending.value = !isAscending.value;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
places.when(
|
||||
data: (places) {
|
||||
if (search.value != null) {
|
||||
@@ -146,41 +78,6 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
||||
.toLowerCase()
|
||||
.contains(search.value!.toLowerCase());
|
||||
}).toList();
|
||||
} else {
|
||||
// Sort based on the selected filter type
|
||||
places = List.from(places);
|
||||
|
||||
if (filterType.value == FilterType.distance &&
|
||||
currentLocation != null) {
|
||||
// Sort places by distance
|
||||
places.sort((a, b) {
|
||||
final double distanceA = calculateDistance(
|
||||
currentLocation!.latitude,
|
||||
currentLocation!.longitude,
|
||||
a.latitude,
|
||||
a.longitude,
|
||||
);
|
||||
final double distanceB = calculateDistance(
|
||||
currentLocation!.latitude,
|
||||
currentLocation!.longitude,
|
||||
b.latitude,
|
||||
b.longitude,
|
||||
);
|
||||
|
||||
return isAscending.value
|
||||
? distanceA.compareTo(distanceB)
|
||||
: distanceB.compareTo(distanceA);
|
||||
});
|
||||
} else {
|
||||
// Sort places by name
|
||||
places.sort(
|
||||
(a, b) => isAscending.value
|
||||
? a.label.toLowerCase().compareTo(b.label.toLowerCase())
|
||||
: b.label
|
||||
.toLowerCase()
|
||||
.compareTo(a.label.toLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/models/places/place_result.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/widgets/search/explore_grid.dart';
|
||||
@@ -14,7 +13,8 @@ class AllPlacesPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
AsyncValue<List<PlaceResult>> places = ref.watch(getAllPlacesProvider);
|
||||
AsyncValue<List<SearchCuratedContent>> places =
|
||||
ref.watch(getAllPlacesProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -28,14 +28,7 @@ class AllPlacesPage extends HookConsumerWidget {
|
||||
),
|
||||
body: places.widgetWhen(
|
||||
onData: (data) => ExploreGrid(
|
||||
curatedContent: data
|
||||
.map(
|
||||
(e) => SearchCuratedContent(
|
||||
label: e.label,
|
||||
id: e.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
curatedContent: data,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -34,8 +34,7 @@ import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MapPage extends HookConsumerWidget {
|
||||
const MapPage({super.key, this.initialLocation});
|
||||
final LatLng? initialLocation;
|
||||
const MapPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -236,8 +235,7 @@ class MapPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void onZoomToLocation() async {
|
||||
final (location, error) =
|
||||
await MapUtils.checkPermAndGetLocation(context: context);
|
||||
final (location, error) = await MapUtils.checkPermAndGetLocation(context);
|
||||
if (error != null) {
|
||||
if (error == LocationPermission.unableToDetermine && context.mounted) {
|
||||
ImmichToast.show(
|
||||
@@ -274,7 +272,6 @@ class MapPage extends HookConsumerWidget {
|
||||
body: Stack(
|
||||
children: [
|
||||
_MapWithMarker(
|
||||
initialLocation: initialLocation,
|
||||
style: style,
|
||||
selectedMarker: selectedMarker,
|
||||
onMapCreated: onMapCreated,
|
||||
@@ -306,7 +303,6 @@ class MapPage extends HookConsumerWidget {
|
||||
body: Stack(
|
||||
children: [
|
||||
_MapWithMarker(
|
||||
initialLocation: initialLocation,
|
||||
style: style,
|
||||
selectedMarker: selectedMarker,
|
||||
onMapCreated: onMapCreated,
|
||||
@@ -372,7 +368,6 @@ class _MapWithMarker extends StatelessWidget {
|
||||
final OnStyleLoadedCallback onStyleLoaded;
|
||||
final Function()? onMarkerTapped;
|
||||
final ValueNotifier<_AssetMarkerMeta?> selectedMarker;
|
||||
final LatLng? initialLocation;
|
||||
|
||||
const _MapWithMarker({
|
||||
required this.style,
|
||||
@@ -382,7 +377,6 @@ class _MapWithMarker extends StatelessWidget {
|
||||
required this.onStyleLoaded,
|
||||
required this.selectedMarker,
|
||||
this.onMarkerTapped,
|
||||
this.initialLocation,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -395,10 +389,8 @@ class _MapWithMarker extends StatelessWidget {
|
||||
children: [
|
||||
style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: initialLocation ?? const LatLng(0, 0),
|
||||
zoom: initialLocation != null ? 12 : 0,
|
||||
),
|
||||
initialCameraPosition:
|
||||
const CameraPosition(target: LatLng(0, 0)),
|
||||
styleString: style,
|
||||
// This is needed to update the selectedMarker's position on map camera updates
|
||||
// The changes are notified through the mapController ValueListener which is added in [onMapCreated]
|
||||
|
||||
@@ -46,7 +46,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
||||
|
||||
Future<void> getCurrentLocation() async {
|
||||
var (currentLocation, _) =
|
||||
await MapUtils.checkPermAndGetLocation(context: context);
|
||||
await MapUtils.checkPermAndGetLocation(context);
|
||||
|
||||
if (currentLocation == null) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/places/place_result.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||
|
||||
import 'package:immich_mobile/services/search.service.dart';
|
||||
@@ -30,7 +29,7 @@ final getPreviewPlacesProvider =
|
||||
});
|
||||
|
||||
final getAllPlacesProvider =
|
||||
FutureProvider.autoDispose<List<PlaceResult>>((ref) async {
|
||||
FutureProvider.autoDispose<List<SearchCuratedContent>>((ref) async {
|
||||
final SearchService searchService = ref.watch(searchServiceProvider);
|
||||
|
||||
final assetPlaces = await searchService.getAllPlaces();
|
||||
@@ -41,11 +40,9 @@ final getAllPlacesProvider =
|
||||
|
||||
final curatedContent = assetPlaces
|
||||
.map(
|
||||
(data) => PlaceResult(
|
||||
(data) => SearchCuratedContent(
|
||||
label: data.exifInfo!.city!,
|
||||
id: data.id,
|
||||
latitude: data.exifInfo!.latitude!.toDouble(),
|
||||
longitude: data.exifInfo!.longitude!.toDouble(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -1024,17 +1024,10 @@ class MapLocationPickerRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [MapPage]
|
||||
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
||||
MapRoute({
|
||||
Key? key,
|
||||
LatLng? initialLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
class MapRoute extends PageRouteInfo<void> {
|
||||
const MapRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
MapRoute.name,
|
||||
args: MapRouteArgs(
|
||||
key: key,
|
||||
initialLocation: initialLocation,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@@ -1043,32 +1036,11 @@ class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args =
|
||||
data.argsAs<MapRouteArgs>(orElse: () => const MapRouteArgs());
|
||||
return MapPage(
|
||||
key: args.key,
|
||||
initialLocation: args.initialLocation,
|
||||
);
|
||||
return const MapPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class MapRouteArgs {
|
||||
const MapRouteArgs({
|
||||
this.key,
|
||||
this.initialLocation,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? initialLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [MemoryPage]
|
||||
class MemoryRoute extends PageRouteInfo<MemoryRouteArgs> {
|
||||
@@ -1361,17 +1333,10 @@ class PhotosRoute extends PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [PlacesCollectionPage]
|
||||
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||
PlacesCollectionRoute({
|
||||
Key? key,
|
||||
LatLng? currentLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
class PlacesCollectionRoute extends PageRouteInfo<void> {
|
||||
const PlacesCollectionRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PlacesCollectionRoute.name,
|
||||
args: PlacesCollectionRouteArgs(
|
||||
key: key,
|
||||
currentLocation: currentLocation,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
@@ -1380,32 +1345,11 @@ class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<PlacesCollectionRouteArgs>(
|
||||
orElse: () => const PlacesCollectionRouteArgs());
|
||||
return PlacesCollectionPage(
|
||||
key: args.key,
|
||||
currentLocation: args.currentLocation,
|
||||
);
|
||||
return const PlacesCollectionPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class PlacesCollectionRouteArgs {
|
||||
const PlacesCollectionRouteArgs({
|
||||
this.key,
|
||||
this.currentLocation,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? currentLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RecentlyAddedPage]
|
||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
// Add method to calculate distance between two LatLng points using Haversine formula
|
||||
double calculateDistance(
|
||||
double? latitude1,
|
||||
double? longitude1,
|
||||
double? latitude2,
|
||||
double? longitude2,
|
||||
) {
|
||||
if (latitude1 == null ||
|
||||
longitude1 == null ||
|
||||
latitude2 == null ||
|
||||
longitude2 == null) {
|
||||
return double.maxFinite;
|
||||
}
|
||||
|
||||
const int earthRadius = 6371; // Earth's radius in kilometers
|
||||
final double lat1 = latitude1 * (pi / 180);
|
||||
final double lat2 = latitude2 * (pi / 180);
|
||||
final double lon1 = longitude1 * (pi / 180);
|
||||
final double lon2 = longitude2 * (pi / 180);
|
||||
|
||||
final double dLat = lat2 - lat1;
|
||||
final double dLon = lon2 - lon1;
|
||||
|
||||
final double a =
|
||||
pow(sin(dLat / 2), 2) + cos(lat1) * cos(lat2) * pow(sin(dLon / 2), 2);
|
||||
final double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
return earthRadius * c;
|
||||
}
|
||||
@@ -64,13 +64,12 @@ class MapUtils {
|
||||
'features': markers.map(_addFeature).toList(),
|
||||
};
|
||||
|
||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
||||
required BuildContext context,
|
||||
bool silent = false,
|
||||
}) async {
|
||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation(
|
||||
BuildContext context,
|
||||
) async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled && !silent) {
|
||||
if (!serviceEnabled) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _LocationServiceDisabledDialog(),
|
||||
@@ -81,7 +80,7 @@ class MapUtils {
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
bool shouldRequestPermission = false;
|
||||
|
||||
if (permission == LocationPermission.denied && !silent) {
|
||||
if (permission == LocationPermission.denied) {
|
||||
shouldRequestPermission = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => _LocationPermissionDisabledDialog(),
|
||||
@@ -95,19 +94,15 @@ class MapUtils {
|
||||
permission == LocationPermission.deniedForever) {
|
||||
// Open app settings only if you did not request for permission before
|
||||
if (permission == LocationPermission.deniedForever &&
|
||||
!shouldRequestPermission &&
|
||||
!silent) {
|
||||
!shouldRequestPermission) {
|
||||
await Geolocator.openAppSettings();
|
||||
}
|
||||
return (null, LocationPermission.deniedForever);
|
||||
}
|
||||
|
||||
Position currentUserLocation = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 0,
|
||||
timeLimit: Duration(seconds: 5),
|
||||
),
|
||||
desiredAccuracy: LocationAccuracy.medium,
|
||||
timeLimit: const Duration(seconds: 5),
|
||||
);
|
||||
return (currentUserLocation, null);
|
||||
} catch (error, stack) {
|
||||
|
||||
@@ -46,39 +46,12 @@ class MapAssetGrid extends HookConsumerWidget {
|
||||
final gridScrollThrottler =
|
||||
useThrottler(interval: const Duration(milliseconds: 300));
|
||||
|
||||
// Add a cache for assets we've already loaded
|
||||
final assetCache = useRef<Map<String, Asset>>({});
|
||||
|
||||
void handleMapEvents(MapEvent event) async {
|
||||
if (event is MapAssetsInBoundsUpdated) {
|
||||
final assetIds = event.assetRemoteIds;
|
||||
final missingIds = <String>[];
|
||||
final currentAssets = <Asset>[];
|
||||
|
||||
for (final id in assetIds) {
|
||||
final asset = assetCache.value[id];
|
||||
if (asset != null) {
|
||||
currentAssets.add(asset);
|
||||
} else {
|
||||
missingIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Only fetch missing assets
|
||||
if (missingIds.isNotEmpty) {
|
||||
final newAssets =
|
||||
await ref.read(dbProvider).assets.getAllByRemoteId(missingIds);
|
||||
|
||||
// Add new assets to cache and current list
|
||||
for (final asset in newAssets) {
|
||||
if (asset.remoteId != null) {
|
||||
assetCache.value[asset.remoteId!] = asset;
|
||||
currentAssets.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assetsInBounds.value = currentAssets;
|
||||
assetsInBounds.value = await ref
|
||||
.read(dbProvider)
|
||||
.assets
|
||||
.getAllByRemoteId(event.assetRemoteIds);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -151,7 +124,7 @@ class MapAssetGrid extends HookConsumerWidget {
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: FractionallySizedBox(
|
||||
// Place it just below the drag handle
|
||||
heightFactor: 0.87,
|
||||
heightFactor: 0.80,
|
||||
child: assetsInBounds.value.isNotEmpty
|
||||
? ref
|
||||
.watch(assetsTimelineProvider(assetsInBounds.value))
|
||||
@@ -278,18 +251,8 @@ class _MapSheetDragRegion extends StatelessWidget {
|
||||
const SizedBox(height: 15),
|
||||
const CustomDraggingHandle(),
|
||||
const SizedBox(height: 15),
|
||||
Center(
|
||||
child: Text(
|
||||
assetsInBoundsText,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: context.textTheme.displayLarge?.color
|
||||
?.withValues(alpha: 0.75),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(assetsInBoundsText, style: context.textTheme.bodyLarge),
|
||||
const Divider(height: 35),
|
||||
],
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
@@ -297,14 +260,14 @@ class _MapSheetDragRegion extends StatelessWidget {
|
||||
builder: (_, value, __) => Visibility(
|
||||
visible: value != null,
|
||||
child: Positioned(
|
||||
right: 18,
|
||||
top: 24,
|
||||
right: 15,
|
||||
top: 15,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.map_outlined,
|
||||
color: context.textTheme.displayLarge?.color,
|
||||
),
|
||||
iconSize: 24,
|
||||
iconSize: 20,
|
||||
tooltip: 'Zoom to bounds',
|
||||
onPressed: () => onZoomToAsset?.call(value!),
|
||||
),
|
||||
|
||||
@@ -20,7 +20,7 @@ class SearchMapThumbnail extends StatelessWidget {
|
||||
return ThumbnailWithInfoContainer(
|
||||
label: 'search_page_your_map'.tr(),
|
||||
onTap: () {
|
||||
context.pushRoute(MapRoute());
|
||||
context.pushRoute(const MapRoute());
|
||||
},
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
|
||||
@@ -696,18 +696,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822
|
||||
sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.0.0"
|
||||
version: "11.1.0"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_android
|
||||
sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4"
|
||||
sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1+1"
|
||||
version: "4.6.1"
|
||||
geolocator_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -728,10 +728,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_web
|
||||
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
|
||||
sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.3"
|
||||
version: "3.0.0"
|
||||
geolocator_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -35,7 +35,7 @@ dependencies:
|
||||
flutter_udid: ^3.0.0
|
||||
flutter_web_auth_2: ^5.0.0-alpha.0
|
||||
fluttertoast: ^8.2.12
|
||||
geolocator: ^14.0.0
|
||||
geolocator: ^11.0.0
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.3.0
|
||||
image_picker: ^1.1.2
|
||||
|
||||
Reference in New Issue
Block a user