From 3e5183606c7d2f7b73c2d421db75624a2acc870f Mon Sep 17 00:00:00 2001 From: xethlyx <46338199+xethlyx@users.noreply.github.com> Date: Sat, 30 Mar 2024 21:57:19 -0400 Subject: [PATCH 01/16] docs: fix typo (#8396) --- docs/docs/guides/remote-access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index fef680d26..766318d5a 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -56,4 +56,4 @@ A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn ### Cons - Complex configuration -- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out. +- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active development and the existence of severe security vulnerabilities cannot be ruled out. From 395c28f5fab702a16fbeab7ce23fd69ad2160141 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:29:02 -0400 Subject: [PATCH 02/16] fix(server): parameter for all places query (#8346) * fix parameter * add e2e --- e2e/src/api/specs/search.e2e-spec.ts | 136 ++++++++++++++++++++++++--- e2e/src/utils.ts | 4 +- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 9c554abc5..afe131228 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -1,4 +1,4 @@ -import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets } from '@immich/sdk'; +import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk'; import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; @@ -7,7 +7,6 @@ import { errorDto } from 'src/responses'; import { app, asBearerAuth, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - const today = DateTime.now(); describe('/search', () => { @@ -19,7 +18,7 @@ describe('/search', () => { let assetCyclamen: AssetFileUploadResponseDto; let assetNotocactus: AssetFileUploadResponseDto; let assetSilver: AssetFileUploadResponseDto; - // let assetDensity: AssetFileUploadResponseDto; + let assetDensity: AssetFileUploadResponseDto; // let assetPhiladelphia: AssetFileUploadResponseDto; // let assetOrychophragmus: AssetFileUploadResponseDto; // let assetRidge: AssetFileUploadResponseDto; @@ -79,6 +78,37 @@ describe('/search', () => { await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id }); } + // note: the coordinates here are not the actual coordinates of the images and are random for most of them + const cities = [ + { latitude: 48.853_41, longitude: 2.3488 }, // paris + { latitude: 63.0695, longitude: -151.0074 }, // denali + { latitude: 52.524_37, longitude: 13.410_53 }, // berlin + { latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore + { latitude: 41.013_84, longitude: 28.949_66 }, // istanbul + { latitude: 5.556_02, longitude: -0.1969 }, // accra + { latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia + { latitude: 23.133_02, longitude: -82.383_04 }, // havana + { latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi + { latitude: 31.222_22, longitude: 121.458_06 }, // shanghai + { latitude: 47.040_57, longitude: 9.068_04 }, // glarus + { latitude: 38.9711, longitude: -109.7137 }, // thompson springs + { latitude: 40.714_27, longitude: -74.005_97 }, // new york + { latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia + { latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh + { latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge + { latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg + { latitude: 35.6895, longitude: 139.691_71 }, // tokyo + ]; + + const updates = assets.map((asset, i) => + updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }), + ); + + await Promise.all(updates); + for (const asset of assets) { + await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); + } + [ assetFalcon, assetDenali, @@ -92,7 +122,7 @@ describe('/search', () => { assetOneJpg5, assetGlarus, assetSprings, - // assetDensity, + assetDensity, // assetPhiladelphia, // assetOrychophragmus, // assetRidge, @@ -106,7 +136,7 @@ describe('/search', () => { }); afterAll(async () => { - await utils.disconnectWebsocket(websocket); + utils.disconnectWebsocket(websocket); }); describe('POST /search/metadata', () => { @@ -298,15 +328,15 @@ describe('/search', () => { }, { should: 'should search by city', - deferred: () => ({ dto: { city: 'Ralston' }, assets: [assetHeic] }), + deferred: () => ({ dto: { city: 'Accra' }, assets: [assetHeic] }), }, { should: 'should search by state', - deferred: () => ({ dto: { state: 'Douglas County, Nebraska' }, assets: [assetHeic] }), + deferred: () => ({ dto: { state: 'New York' }, assets: [assetDensity] }), }, { should: 'should search by country', - deferred: () => ({ dto: { country: 'United States of America' }, assets: [assetHeic] }), + deferred: () => ({ dto: { country: 'France' }, assets: [assetFalcon] }), }, { should: 'should search by make', @@ -370,13 +400,44 @@ describe('/search', () => { expect(body).toEqual(errorDto.unauthorized); }); - it('should get places', async () => { + it('should get relevant places', async () => { + const name = 'Paris'; + const { status, body } = await request(app) - .get('/search/places?name=Paris') + .get(`/search/places?name=${name}`) .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); expect(Array.isArray(body)).toBe(true); - expect(body.length).toBeGreaterThan(10); + if (Array.isArray(body)) { + expect(body.length).toBeGreaterThan(10); + expect(body[0].name).toEqual(name); + expect(body[0].admin2name).toEqual(name); + } + }); + }); + + describe('GET /search/cities', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/search/cities'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get all cities', async () => { + const { status, body } = await request(app) + .get('/search/cities') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(Array.isArray(body)).toBe(true); + if (Array.isArray(body)) { + expect(body.length).toBeGreaterThan(10); + const assetsWithCity = body.filter((asset) => !!asset.exifInfo?.city); + expect(assetsWithCity.length).toEqual(body.length); + const cities = new Set(assetsWithCity.map((asset) => asset.exifInfo.city)); + expect(cities.size).toEqual(body.length); + } }); }); @@ -391,7 +452,21 @@ describe('/search', () => { const { status, body } = await request(app) .get('/search/suggestions?type=country') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(['United States of America']); + expect(body).toEqual([ + 'Cuba', + 'France', + 'Georgia', + 'Germany', + 'Ghana', + 'Japan', + 'Morocco', + "People's Republic of China", + 'Russian Federation', + 'Singapore', + 'Spain', + 'Switzerland', + 'United States of America', + ]); expect(status).toBe(200); }); @@ -399,7 +474,23 @@ describe('/search', () => { const { status, body } = await request(app) .get('/search/suggestions?type=state') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']); + expect(body).toEqual([ + 'Accra, Greater Accra', + 'Berlin', + 'Glarus, Glarus', + 'Havana', + 'Marrakech, Marrakesh-Safi', + 'Mesa County, Colorado', + 'Neshoba County, Mississippi', + 'New York', + 'Page County, Virginia', + 'Paris, Île-de-France', + 'Province of Córdoba, Andalusia', + 'Shanghai Municipality, Shanghai', + 'St.-Petersburg', + 'Tbilisi', + 'Tokyo', + ]); expect(status).toBe(200); }); @@ -407,7 +498,24 @@ describe('/search', () => { const { status, body } = await request(app) .get('/search/suggestions?type=city') .set('Authorization', `Bearer ${admin.accessToken}`); - expect(body).toEqual(['Palisade', 'Ralston']); + expect(body).toEqual([ + 'Accra', + 'Berlin', + 'Glarus', + 'Havana', + 'Marrakesh', + 'Montalbán de Córdoba', + 'New York City', + 'Palisade', + 'Paris', + 'Philadelphia', + 'Saint Petersburg', + 'Shanghai', + 'Singapore', + 'Stanley', + 'Tbilisi', + 'Tokyo', + ]); expect(status).toBe(200); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 6b538129a..d8302a9e3 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -39,7 +39,7 @@ import { makeRandomImage } from 'src/generators'; import request from 'supertest'; type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; -type EventType = 'assetUpload' | 'assetDelete' | 'userDelete'; +type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete'; type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number }; type AdminSetupOptions = { onboarding?: boolean }; type AssetData = { bytes?: Buffer; filename: string }; @@ -82,6 +82,7 @@ let client: pg.Client | null = null; const events: Record> = { assetUpload: new Set(), + assetUpdate: new Set(), assetDelete: new Set(), userDelete: new Set(), }; @@ -185,6 +186,7 @@ export const utils = { websocket .on('connect', () => resolve(websocket)) .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id })) + .on('on_asset_update', (data: AssetResponseDto) => onEvent({ event: 'assetUpdate', id: data.id })) .on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId })) .on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId })) .connect(); From 8e5695f06df6ca6deaf81b8df103268b3ac3dee5 Mon Sep 17 00:00:00 2001 From: mmomjian <50788000+mmomjian@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:35:06 -0400 Subject: [PATCH 03/16] Add docs for Postgres standalone setup (#8343) * Create postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md * Update docs/docs/administration/postgres-standalone.md Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * Update docs/docs/administration/postgres-standalone.md Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * Update docs/docs/administration/postgres-standalone.md Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * Update docs/docs/administration/postgres-standalone.md Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * Update docs/docs/administration/postgres-standalone.md Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * Update postgres-standalone.md * Update postgres-standalone.md Planning to write a guide in the future about setting up streaming database backups * Update postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md * Update postgres-standalone.md --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> --- .../administration/postgres-standalone.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/docs/administration/postgres-standalone.md diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md new file mode 100644 index 000000000..4c3c77455 --- /dev/null +++ b/docs/docs/administration/postgres-standalone.md @@ -0,0 +1,54 @@ +# Preparing a pre-existing Postgres server + +While not officially recommended, it is possible to run Immich using a pre-existing Postgres server. To use this setup, you should have a baseline level of familiarity with Postgres and the Linux command line. If you do not have these, we recommend using the default setup with a dedicated Postgres container. + +By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich. + +:::tip +Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication, data page checksums, and streaming write-ahead log backups using programs like pgBackRest or Barman. +::: + +## Prerequisites + +You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`. + +:::note +Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`. +::: + +## Specifying the connection URL + +You can connect to your pre-existing Postgres server by setting the `DB_URL` environment variable in the `.env` file. + +``` +DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename' + +# require a SSL connection to Postgres +# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require' + +# require a SSL connection, but don't enforce checking the certificate name +# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify' +``` + +## Without superuser permissions + +### Initial installation + +Immich can run without superuser permissions by following the below instructions at the `psql` prompt to prepare the database. + +```sql title="Set up Postgres for Immich" +CREATE DATABASE ; +\c +BEGIN; +ALTER DATABASE OWNER TO ; +CREATE EXTENSION vectors; +CREATE EXTENSION earthdistance CASCADE; +ALTER DATABASE SET search_path TO "$user", public, vectors; +GRANT USAGE ON SCHEMA vectors TO ; +GRANT SELECT ON TABLE pg_vector_index_stat to ; +COMMIT; +``` + +### Updating pgvecto.rs + +When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`. From c4bb9f49ff748d7660dc41434b6c4162af80678f Mon Sep 17 00:00:00 2001 From: mmomjian <50788000+mmomjian@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:47:00 -0400 Subject: [PATCH 04/16] Fix repair page typo (#8401) Update +page.svelte --- web/src/routes/admin/repair/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte index 50ffeb581..14430a099 100644 --- a/web/src/routes/admin/repair/+page.svelte +++ b/web/src/routes/admin/repair/+page.svelte @@ -250,7 +250,7 @@

OFFLINE PATHS {orphans.length > 0 ? `(${orphans.length})` : ''}

- These files are the results of manually deletion of the default upload library + These results may be due to manual deletion of files in the default upload library

From b6af7788e18ad72cdb5cd90759c681f1533830a1 Mon Sep 17 00:00:00 2001 From: mmomjian <50788000+mmomjian@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:48:01 -0400 Subject: [PATCH 05/16] feat(server): extensions for MPEG and 3GP (#8400) * Update mime-types.spec.ts * Update mime-types.ts --- server/src/utils/mime-types.spec.ts | 3 +++ server/src/utils/mime-types.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index 96c1921ba..bce75e1e1 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -75,10 +75,13 @@ describe('mimeTypes', () => { { mimetype: 'image/x-sony-srf', extension: '.srf' }, { mimetype: 'image/x3f', extension: '.x3f' }, { mimetype: 'video/3gpp', extension: '.3gp' }, + { mimetype: 'video/3gpp', extension: '.3gpp' }, { mimetype: 'video/avi', extension: '.avi' }, { mimetype: 'video/mp2t', extension: '.m2ts' }, { mimetype: 'video/mp2t', extension: '.mts' }, { mimetype: 'video/mp4', extension: '.mp4' }, + { mimetype: 'video/mpeg', extension: '.mpe' }, + { mimetype: 'video/mpeg', extension: '.mpeg' }, { mimetype: 'video/mpeg', extension: '.mpg' }, { mimetype: 'video/msvideo', extension: '.avi' }, { mimetype: 'video/quicktime', extension: '.mov' }, diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 789fc6a1c..a888e4f42 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -56,6 +56,7 @@ const profile: Record = Object.fromEntries( const video: Record = { '.3gp': ['video/3gpp'], + '.3gpp': ['video/3gpp'], '.avi': ['video/avi', 'video/msvideo', 'video/vnd.avi', 'video/x-msvideo'], '.flv': ['video/x-flv'], '.insv': ['video/mp4'], @@ -64,6 +65,8 @@ const video: Record = { '.mkv': ['video/x-matroska'], '.mov': ['video/quicktime'], '.mp4': ['video/mp4'], + '.mpe': ['video/mpeg'], + '.mpeg': ['video/mpeg'], '.mpg': ['video/mpeg'], '.mts': ['video/mp2t'], '.webm': ['video/webm'], From 94cd8066757975f21354e94f9a5e67a243cb6b4c Mon Sep 17 00:00:00 2001 From: mmomjian <50788000+mmomjian@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:48:37 -0400 Subject: [PATCH 06/16] docs: Nginx config update (#8397) * Update reverse-proxy.md * Update reverse-proxy.md * Update reverse-proxy.md * Update reverse-proxy.md * Update reverse-proxy.md * Update reverse-proxy.md * Update reverse-proxy.md * Update reverse-proxy.md --- docs/docs/administration/reverse-proxy.md | 46 ++++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 24919347f..1d2488f11 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -1,29 +1,41 @@ # Reverse Proxy -Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich. +Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Real-IP`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich. + +:::note +The Repair page can take a long time to load. To avoid server timeouts or errors, we recommend specifying a timeout of at least 10 minutes on your proxy server. +::: ### Nginx example config -Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`. +Below is an example config for nginx. Make sure to set `public_url` to the front-facing URL of your instance, and `backend_url` to the path of the Immich server. ```nginx server { - server_name + server_name ; + # allow large file uploads client_max_body_size 50000M; - location / { - proxy_pass http://:2283; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + # Set headers + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; - # http://nginx.org/en/docs/http/websocket.html - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_redirect off; + # enable websockets: http://nginx.org/en/docs/http/websocket.html + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + + # set timeout + proxy_read_timeout 600s; + proxy_send_timeout 600s; + send_timeout 600s; + + location / { + proxy_pass http://:2283; } } ``` @@ -42,15 +54,13 @@ immich.example.org { Below is an example config for Apache2 site configuration. -``` +```ApacheConf ServerName ProxyRequests Off + # set timeout in seconds ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket ProxyPassReverse / http://127.0.0.1:2283/ ProxyPreserveHost On - ``` - -**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error. From e2d5a8c0bbae956d9150624355382dc26ba8779d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 31 Mar 2024 06:05:18 +0000 Subject: [PATCH 07/16] fix(deps): update machine-learning (#8280) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/Dockerfile | 4 +- machine-learning/export/Dockerfile | 2 +- machine-learning/poetry.lock | 79 +++++++++++++++--------------- 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 2df540f09..3f111401c 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:991e20a11120277e977cadbc104e7a9b196a68a346597879821b19034285a403 as builder-cpu +FROM python:3.11-bookworm@sha256:e2ed446c899827ed992f8a5a8875fa0853fcab32581e61418b650322061aa3c4 as builder-cpu FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino USER root @@ -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:a2eb07f336e4f194358382611b4fea136c632b40baa6314cb27a366deeaf0144 as prod-cpu +FROM python:3.11-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as prod-cpu FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino USER root diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index b9aa8f1ed..fa3f7068e 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:881dbb68d115182b2c12e7e77dc54ea5005fd4e0123ca009d822adb5b0631785 as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:3624db3aee11d2f3f00d25f691aaaf8834b8bc4ec1b340dcdb48ef37281ea604 as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 8d53fc84e..54dd16292 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1274,13 +1274,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "huggingface-hub" -version = "0.21.4" +version = "0.22.2" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.21.4-py3-none-any.whl", hash = "sha256:df37c2c37fc6c82163cdd8a67ede261687d80d1e262526d6c0ce73b6b3630a7b"}, - {file = "huggingface_hub-0.21.4.tar.gz", hash = "sha256:e1f4968c93726565a80edf6dc309763c7b546d0cfe79aa221206034d50155531"}, + {file = "huggingface_hub-0.22.2-py3-none-any.whl", hash = "sha256:3429e25f38ccb834d310804a3b711e7e4953db5a9e420cc147a5e194ca90fd17"}, + {file = "huggingface_hub-0.22.2.tar.gz", hash = "sha256:32e9a9a6843c92f253ff9ca16b9985def4d80a93fb357af5353f770ef74a81be"}, ] [package.dependencies] @@ -1293,15 +1293,16 @@ tqdm = ">=4.42.1" typing-extensions = ">=3.7.4.3" [package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] hf-transfer = ["hf-transfer (>=0.1.4)"] -inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"] -quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"] +inference = ["aiohttp", "minijinja (>=1.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"] tensorflow = ["graphviz", "pydot", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] torch = ["safetensors", "torch"] typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] @@ -1567,13 +1568,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.24.0" +version = "2.24.1" description = "Developer friendly load testing framework" optional = false python-versions = ">=3.8" files = [ - {file = "locust-2.24.0-py3-none-any.whl", hash = "sha256:1b6b878b4fd0108fec956120815e69775d2616c8f4d1e9f365c222a7a5c17d9a"}, - {file = "locust-2.24.0.tar.gz", hash = "sha256:6cffa378d995244a7472af6be1d6139331f19aee44e907deee73e0281252804d"}, + {file = "locust-2.24.1-py3-none-any.whl", hash = "sha256:7f6ed4dc289aad66c304582e6d25e4de5d7c3b175b580332442ab2be35b9d916"}, + {file = "locust-2.24.1.tar.gz", hash = "sha256:094161d44d94839bf1120fd7898b7abb9c143833743ba7c096beb470a236b9a7"}, ] [package.dependencies] @@ -2495,13 +2496,13 @@ testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygm [[package]] name = "pytest-asyncio" -version = "0.23.5.post1" +version = "0.23.6" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, - {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, ] [package.dependencies] @@ -2531,17 +2532,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.12.0" +version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] -pytest = ">=5.0" +pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] @@ -2844,28 +2845,28 @@ files = [ [[package]] name = "ruff" -version = "0.3.3" +version = "0.3.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, - {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, - {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, - {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, - {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, - {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, + {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, + {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, + {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, + {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, ] [[package]] @@ -3289,13 +3290,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.28.0" +version = "0.29.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, - {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, ] [package.dependencies] From 34cbb18ecdf5577163eccd6517ccf3ed9f5e715f Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Sun, 31 Mar 2024 08:59:11 +0200 Subject: [PATCH 08/16] fix(mobile): memories translation (#8316) --- mobile/assets/i18n/en-US.json | 6 +++++- .../modules/memories/ui/memory_epilogue.dart | 17 +++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 3bebcb2b1..e350e5b62 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -283,6 +283,10 @@ "map_settings_only_show_favorites": "Show Favorite Only", "map_settings_theme_settings": "Map Theme", "map_zoom_to_see_photos": "Zoom out to see photos", + "memories_all_caught_up": "All caught up", + "memories_check_back_tomorrow": "Check back tomorrow for more memories", + "memories_start_over": "Start Over", + "memories_swipe_to_close": "Swipe up to close", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Motion Photos", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", @@ -482,4 +486,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} +} \ No newline at end of file diff --git a/mobile/lib/modules/memories/ui/memory_epilogue.dart b/mobile/lib/modules/memories/ui/memory_epilogue.dart index 8dd28637d..b817d67f0 100644 --- a/mobile/lib/modules/memories/ui/memory_epilogue.dart +++ b/mobile/lib/modules/memories/ui/memory_epilogue.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -55,27 +56,27 @@ class _MemoryEpilogueState extends State ), const SizedBox(height: 16.0), Text( - 'All caught up', + "memories_all_caught_up", style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: Colors.white, ), - ), + ).tr(), const SizedBox(height: 16.0), Text( - 'Check back tomorrow for more memories', + "memories_check_back_tomorrow", style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white, ), - ), + ).tr(), const SizedBox(height: 16.0), TextButton( onPressed: widget.onStartOver, child: Text( - 'Start Over', + "memories_start_over", style: context.textTheme.displayMedium?.copyWith( color: immichDarkThemePrimaryColor, ), - ), + ).tr(), ), ], ), @@ -106,11 +107,11 @@ class _MemoryEpilogueState extends State ), ), Text( - 'Swipe up to close', + "memories_swipe_to_close", style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white, ), - ), + ).tr(), ], ), ), From 6a4bc777a2eb2ba2d4570c3b2e1f13cd46b3f2cf Mon Sep 17 00:00:00 2001 From: Pablo Diz <87752439+pablodre@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:47:03 +0200 Subject: [PATCH 09/16] Fix external library path validation #8319 (#8366) * Fix isImmichPath * prettier write * Fis isImmichPath code comment * Refactor isImmichPath function based on team suggestions * Test isImmichPath * fix: clean comments * Refactor isImmichPath test based on team suggestions * Clean code with lintern suggestions --- server/src/cores/storage.core.spec.ts | 29 +++++++++++++++++++++++++++ server/src/cores/storage.core.ts | 8 +++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 server/src/cores/storage.core.spec.ts diff --git a/server/src/cores/storage.core.spec.ts b/server/src/cores/storage.core.spec.ts new file mode 100644 index 000000000..16258f095 --- /dev/null +++ b/server/src/cores/storage.core.spec.ts @@ -0,0 +1,29 @@ +import { StorageCore } from 'src/cores/storage.core'; + +jest.mock('src/constants', () => ({ + APP_MEDIA_LOCATION: '/photos', +})); + +describe('StorageCore', () => { + describe('isImmichPath', () => { + it('should return true for APP_MEDIA_LOCATION path', () => { + const immichPath = '/photos'; + expect(StorageCore.isImmichPath(immichPath)).toBe(true); + }); + + it('should return true for paths within the APP_MEDIA_LOCATION', () => { + const immichPath = '/photos/new/'; + expect(StorageCore.isImmichPath(immichPath)).toBe(true); + }); + + it('should return false for paths outside the APP_MEDIA_LOCATION and same starts', () => { + const nonImmichPath = '/photos_new'; + expect(StorageCore.isImmichPath(nonImmichPath)).toBe(false); + }); + + it('should return false for paths outside the APP_MEDIA_LOCATION', () => { + const nonImmichPath = '/some/other/path'; + expect(StorageCore.isImmichPath(nonImmichPath)).toBe(false); + }); + }); +}); diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index b9dad8642..ee9f12e51 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -115,7 +115,13 @@ export class StorageCore { } static isImmichPath(path: string) { - return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION)); + const resolvedPath = resolve(path); + const resolvedAppMediaLocation = resolve(APP_MEDIA_LOCATION); + const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/'; + const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/') + ? resolvedAppMediaLocation + : resolvedAppMediaLocation + '/'; + return normalizedPath.startsWith(normalizedAppMediaLocation); } static isGeneratedAsset(path: string) { From 5bc9158724308bbd4bcf0f448c2ae845c0a05653 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:59:11 -0400 Subject: [PATCH 10/16] fix(server): penalize null geodata fields when searching places (#8408) --- server/src/queries/search.repository.sql | 8 ++++---- server/src/repositories/search.repository.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index c7b660068..e985a1a6d 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -254,15 +254,15 @@ WHERE OR f_unaccent ("admin1Name") %>> f_unaccent ($1) OR f_unaccent ("alternateNames") %>> f_unaccent ($1) ORDER BY - COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0) + COALESCE( + COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0.1) + COALESCE( f_unaccent ("admin2Name") <->>> f_unaccent ($1), - 0 + 0.1 ) + COALESCE( f_unaccent ("admin1Name") <->>> f_unaccent ($1), - 0 + 0.1 ) + COALESCE( f_unaccent ("alternateNames") <->>> f_unaccent ($1), - 0 + 0.1 ) ASC LIMIT 20 diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 2de48b741..4530d2295 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -214,10 +214,10 @@ export class SearchRepository implements ISearchRepository { .orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`) .orderBy( ` - COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0) + - COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0) + - COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0) + - COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0) + COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0.1) + + COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0.1) + + COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0.1) + + COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0.1) `, ) .setParameters({ placeName }) From 245535ee0413e6ee4793d092f39effc7f291d4b5 Mon Sep 17 00:00:00 2001 From: mmomjian <50788000+mmomjian@users.noreply.github.com> Date: Sun, 31 Mar 2024 12:38:16 -0400 Subject: [PATCH 11/16] docs: specify Timezone (#8403) --- docs/docs/install/environment-variables.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 9fc1b20d2..ea9608c56 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -41,11 +41,9 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices | :::tip +`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`. -`TZ` is only used by the `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. - -`exiftool` is only present in the microservices container. - +`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata. ::: ## Ports From 169d9d18b08cd2f5117a29f82c9a81a5e528159e Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sun, 31 Mar 2024 13:29:11 -0400 Subject: [PATCH 12/16] docs: document metric env variables, add job metric env (#8406) * update env docs * show options --- docs/docs/features/monitoring.md | 4 ++-- docs/docs/install/environment-variables.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/docs/features/monitoring.md b/docs/docs/features/monitoring.md index 7e001c992..0ea1382c8 100644 --- a/docs/docs/features/monitoring.md +++ b/docs/docs/features/monitoring.md @@ -27,8 +27,8 @@ The metrics in immich are grouped into API (endpoint calls and response times), Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable. -:::note -`IMMICH_METRICS` is equivalent to enabling the following three environmental variables: `IMMICH_API_METRICS`, `IMMICH_HOST_METRICS`, and `IMMICH_IO_METRICS`. If you would like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. +:::tip +`IMMICH_METRICS` enables all metrics, but there are also [environmental variables](/docs/install/environment-variables.md#prometheus) to toggle specific metric groups. If you'd like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. For example, setting `IMMICH_METRICS=true` and `IMMICH_API_METRICS=false` will enable all metrics except API metrics. ::: The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way. diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index ea9608c56..9da1f3ce9 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -145,6 +145,18 @@ Other machine learning parameters can be tuned from the admin UI. ::: +## Prometheus + +| Variable | Description | Default | Services | +| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- | +| `IMMICH_METRICS`\*1 | Toggle all metrics (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server, microservices | +| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server, microservices | + +\*1: Overridden for a metric group when its corresponding environmental variable is set. + ## Docker Secrets The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security. From fd83280b70b515098e9cdc4e95de163071013e11 Mon Sep 17 00:00:00 2001 From: mmomjian <50788000+mmomjian@users.noreply.github.com> Date: Sun, 31 Mar 2024 21:52:20 -0400 Subject: [PATCH 13/16] docs: Postgres standalone fix (#8427) --- docs/docs/administration/postgres-standalone.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md index 4c3c77455..c29aa54e6 100644 --- a/docs/docs/administration/postgres-standalone.md +++ b/docs/docs/administration/postgres-standalone.md @@ -1,4 +1,4 @@ -# Preparing a pre-existing Postgres server +# Pre-existing Postgres While not officially recommended, it is possible to run Immich using a pre-existing Postgres server. To use this setup, you should have a baseline level of familiarity with Postgres and the Linux command line. If you do not have these, we recommend using the default setup with a dedicated Postgres container. @@ -45,7 +45,7 @@ CREATE EXTENSION vectors; CREATE EXTENSION earthdistance CASCADE; ALTER DATABASE SET search_path TO "$user", public, vectors; GRANT USAGE ON SCHEMA vectors TO ; -GRANT SELECT ON TABLE pg_vector_index_stat to ; +ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO ; COMMIT; ``` From 861b72ef0493bb6272c1a1b16e5cdee6792fb4b6 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Mon, 1 Apr 2024 06:14:35 +0200 Subject: [PATCH 14/16] fix(mobile): update album date range on add/remove (#8324) --- .../modules/album/services/album.service.dart | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index f66a30f31..a72620b86 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -243,12 +243,7 @@ class AlbumService { } } - await _db.writeTxn(() async { - await album.assets.update(link: successAssets); - final a = await _db.albums.get(album.id); - // trigger watcher - await _db.albums.put(a!); - }); + await _updateAssets(album.id, add: successAssets); return AddAssetsResponse( alreadyInAlbum: duplicatedAssets, @@ -257,11 +252,28 @@ class AlbumService { } } catch (e) { debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}"); - return null; } return null; } + Future _updateAssets( + int albumId, { + Iterable add = const [], + Iterable remove = const [], + }) { + return _db.writeTxn(() async { + final album = await _db.albums.get(albumId); + if (album == null) return; + await album.assets.update(link: add, unlink: remove); + album.startDate = + await album.assets.filter().fileCreatedAtProperty().min(); + album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); + album.lastModifiedAssetTimestamp = + await album.assets.filter().updatedAtProperty().max(); + await _db.albums.put(album); + }); + } + Future addAdditionalUserToAlbum( List sharedUserIds, Album album, @@ -342,7 +354,7 @@ class AlbumService { await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me"); return true; } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); + debugPrint("Error leaveAlbum ${e.toString()}"); return false; } } @@ -352,24 +364,25 @@ class AlbumService { Iterable assets, ) async { try { - await _apiService.albumApi.removeAssetFromAlbum( + final response = await _apiService.albumApi.removeAssetFromAlbum( album.remoteId!, BulkIdsDto( ids: assets.map((asset) => asset.remoteId!).toList(), ), ); - await _db.writeTxn(() async { - await album.assets.update(unlink: assets); - final a = await _db.albums.get(album.id); - // trigger watcher - await _db.albums.put(a!); - }); - - return true; + if (response != null) { + final toRemove = response.every((e) => e.success) + ? assets + : response + .where((e) => e.success) + .map((e) => assets.firstWhere((a) => a.remoteId == e.id)); + await _updateAssets(album.id, remove: toRemove); + return true; + } } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); - return false; + debugPrint("Error removeAssetFromAlbum ${e.toString()}"); } + return false; } Future removeUserFromAlbum( @@ -413,7 +426,7 @@ class AlbumService { return true; } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); + debugPrint("Error changeTitleAlbum ${e.toString()}"); return false; } } From 27be81301175e6698cc0236d7a7679db53d6ded1 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 1 Apr 2024 09:45:11 -0500 Subject: [PATCH 15/16] feat(mobile): search enhancement (#8392) --- mobile/ios/Podfile.lock | 27 +- .../home/ui/asset_grid/immich_asset_grid.dart | 2 +- .../search/models/curated_content.dart | 75 +- .../modules/search/models/search_filter.dart | 310 ++ .../providers/paginated_search.provider.dart | 62 + .../paginated_search.provider.g.dart | 44 + .../search/providers/people.provider.dart | 100 +- .../search/providers/people.provider.g.dart | 24 +- .../providers/search_filter.provider.dart | 27 + .../providers/search_filter.provider.g.dart | 229 ++ .../search_result_page.provider.dart | 67 - .../search/services/person.service.dart | 2 +- .../search/services/search.service.dart | 94 +- .../lib/modules/search/ui/explore_grid.dart | 19 +- .../modules/search/ui/immich_search_bar.dart | 99 - .../ui/search_filter/camera_picker.dart | 120 + .../search_filter/display_option_picker.dart | 68 + .../filter_bottom_sheet_scaffold.dart | 68 + .../ui/search_filter/location_picker.dart | 166 + .../ui/search_filter/media_type_picker.dart | 48 + .../ui/search_filter/people_picker.dart | 81 + .../ui/search_filter/search_filter_chip.dart | 68 + .../ui/search_filter/search_filter_utils.dart | 19 + .../search/ui/search_suggestion_list.dart | 66 - .../modules/search/views/all_people_page.dart | 73 +- .../search/views/search_input_page.dart | 563 +++ .../lib/modules/search/views/search_page.dart | 553 ++- .../search/views/search_result_page.dart | 213 - mobile/lib/routing/router.dart | 12 +- mobile/lib/routing/router.gr.dart | 113 +- .../lib/routing/tab_navigation_observer.dart | 2 +- .../ui/asset_grid/multiselect_grid.dart | 5 +- .../lib/shared/views/tab_controller_page.dart | 18 +- mobile/lib/utils/immich_app_theme.dart | 7 + mobile/pubspec.lock | 3624 ++++++++--------- 35 files changed, 4302 insertions(+), 2766 deletions(-) create mode 100644 mobile/lib/modules/search/models/search_filter.dart create mode 100644 mobile/lib/modules/search/providers/paginated_search.provider.dart create mode 100644 mobile/lib/modules/search/providers/paginated_search.provider.g.dart create mode 100644 mobile/lib/modules/search/providers/search_filter.provider.dart create mode 100644 mobile/lib/modules/search/providers/search_filter.provider.g.dart delete mode 100644 mobile/lib/modules/search/providers/search_result_page.provider.dart delete mode 100644 mobile/lib/modules/search/ui/immich_search_bar.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/camera_picker.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/display_option_picker.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/location_picker.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/media_type_picker.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/people_picker.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart create mode 100644 mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart delete mode 100644 mobile/lib/modules/search/ui/search_suggestion_list.dart create mode 100644 mobile/lib/modules/search/views/search_input_page.dart delete mode 100644 mobile/lib/modules/search/views/search_result_page.dart diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e77749ffd..5493fc284 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -17,6 +17,9 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) - geolocator_apple (1.2.0): - Flutter - image_picker_ios (0.0.1): @@ -36,7 +39,7 @@ PODS: - FlutterMacOS - path_provider_ios (0.0.1): - Flutter - - permission_handler_apple (9.3.0): + - permission_handler_apple (9.1.1): - Flutter - photo_manager (2.0.0): - Flutter @@ -50,7 +53,7 @@ PODS: - FlutterMacOS - sqflite (0.0.3): - Flutter - - FlutterMacOS + - FMDB (>= 2.7.5) - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter @@ -81,13 +84,14 @@ DEPENDENCIES: - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: trunk: + - FMDB - MapLibre - ReachabilitySwift - SAMKeychain @@ -135,7 +139,7 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -151,23 +155,24 @@ SPEC CHECKSUMS: flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461 - image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: 13825b8a9334a850581300559b8839134b124670 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 @@ -175,4 +180,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 687e7aaac..f075280ae 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -42,7 +42,7 @@ class ImmichAssetGrid extends HookConsumerWidget { this.assetsPerRow, this.showStorageIndicator, this.listener, - this.margin = 5.0, + this.margin = 2.0, this.selectionActive = false, this.preselectedAssets, this.canDeselect = true, diff --git a/mobile/lib/modules/search/models/curated_content.dart b/mobile/lib/modules/search/models/curated_content.dart index df7cb032c..87e98bb75 100644 --- a/mobile/lib/modules/search/models/curated_content.dart +++ b/mobile/lib/modules/search/models/curated_content.dart @@ -1,15 +1,60 @@ -/// A wrapper for [CuratedLocationsResponseDto] objects -/// and [CuratedObjectsResponseDto] to be displayed in -/// a view -class CuratedContent { - /// The label to show associated with this curated object - final String label; - - /// The id to lookup the asset from the server - final String id; - - CuratedContent({ - required this.id, - required this.label, - }); -} +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +/// A wrapper for [CuratedLocationsResponseDto] objects +/// and [CuratedObjectsResponseDto] to be displayed in +/// a view +class CuratedContent { + /// The label to show associated with this curated object + final String label; + + /// The id to lookup the asset from the server + final String id; + + CuratedContent({ + required this.label, + required this.id, + }); + + CuratedContent copyWith({ + String? label, + String? id, + }) { + return CuratedContent( + label: label ?? this.label, + id: id ?? this.id, + ); + } + + Map toMap() { + return { + 'label': label, + 'id': id, + }; + } + + factory CuratedContent.fromMap(Map map) { + return CuratedContent( + label: map['label'] as String, + id: map['id'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory CuratedContent.fromJson(String source) => + CuratedContent.fromMap(json.decode(source) as Map); + + @override + String toString() => 'CuratedContent(label: $label, id: $id)'; + + @override + bool operator ==(covariant CuratedContent other) { + if (identical(this, other)) return true; + + return other.label == label && other.id == id; + } + + @override + int get hashCode => label.hashCode ^ id.hashCode; +} diff --git a/mobile/lib/modules/search/models/search_filter.dart b/mobile/lib/modules/search/models/search_filter.dart new file mode 100644 index 000000000..337da9266 --- /dev/null +++ b/mobile/lib/modules/search/models/search_filter.dart @@ -0,0 +1,310 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:openapi/api.dart'; + +class SearchLocationFilter { + String? country; + String? state; + String? city; + SearchLocationFilter({ + this.country, + this.state, + this.city, + }); + + SearchLocationFilter copyWith({ + String? country, + String? state, + String? city, + }) { + return SearchLocationFilter( + country: country ?? this.country, + state: state ?? this.state, + city: city ?? this.city, + ); + } + + Map toMap() { + return { + 'country': country, + 'state': state, + 'city': city, + }; + } + + factory SearchLocationFilter.fromMap(Map map) { + return SearchLocationFilter( + country: map['country'] != null ? map['country'] as String : null, + state: map['state'] != null ? map['state'] as String : null, + city: map['city'] != null ? map['city'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchLocationFilter.fromJson(String source) => + SearchLocationFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'SearchLocationFilter(country: $country, state: $state, city: $city)'; + + @override + bool operator ==(covariant SearchLocationFilter other) { + if (identical(this, other)) return true; + + return other.country == country && + other.state == state && + other.city == city; + } + + @override + int get hashCode => country.hashCode ^ state.hashCode ^ city.hashCode; +} + +class SearchCameraFilter { + String? make; + String? model; + SearchCameraFilter({ + this.make, + this.model, + }); + + SearchCameraFilter copyWith({ + String? make, + String? model, + }) { + return SearchCameraFilter( + make: make ?? this.make, + model: model ?? this.model, + ); + } + + Map toMap() { + return { + 'make': make, + 'model': model, + }; + } + + factory SearchCameraFilter.fromMap(Map map) { + return SearchCameraFilter( + make: map['make'] != null ? map['make'] as String : null, + model: map['model'] != null ? map['model'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchCameraFilter.fromJson(String source) => + SearchCameraFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SearchCameraFilter(make: $make, model: $model)'; + + @override + bool operator ==(covariant SearchCameraFilter other) { + if (identical(this, other)) return true; + + return other.make == make && other.model == model; + } + + @override + int get hashCode => make.hashCode ^ model.hashCode; +} + +class SearchDateFilter { + DateTime? takenBefore; + DateTime? takenAfter; + SearchDateFilter({ + this.takenBefore, + this.takenAfter, + }); + + SearchDateFilter copyWith({ + DateTime? takenBefore, + DateTime? takenAfter, + }) { + return SearchDateFilter( + takenBefore: takenBefore ?? this.takenBefore, + takenAfter: takenAfter ?? this.takenAfter, + ); + } + + Map toMap() { + return { + 'takenBefore': takenBefore?.millisecondsSinceEpoch, + 'takenAfter': takenAfter?.millisecondsSinceEpoch, + }; + } + + factory SearchDateFilter.fromMap(Map map) { + return SearchDateFilter( + takenBefore: map['takenBefore'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['takenBefore'] as int) + : null, + takenAfter: map['takenAfter'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['takenAfter'] as int) + : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchDateFilter.fromJson(String source) => + SearchDateFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)'; + + @override + bool operator ==(covariant SearchDateFilter other) { + if (identical(this, other)) return true; + + return other.takenBefore == takenBefore && other.takenAfter == takenAfter; + } + + @override + int get hashCode => takenBefore.hashCode ^ takenAfter.hashCode; +} + +class SearchDisplayFilters { + bool isNotInAlbum = false; + bool isArchive = false; + bool isFavorite = false; + SearchDisplayFilters({ + required this.isNotInAlbum, + required this.isArchive, + required this.isFavorite, + }); + + SearchDisplayFilters copyWith({ + bool? isNotInAlbum, + bool? isArchive, + bool? isFavorite, + }) { + return SearchDisplayFilters( + isNotInAlbum: isNotInAlbum ?? this.isNotInAlbum, + isArchive: isArchive ?? this.isArchive, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + + Map toMap() { + return { + 'isNotInAlbum': isNotInAlbum, + 'isArchive': isArchive, + 'isFavorite': isFavorite, + }; + } + + factory SearchDisplayFilters.fromMap(Map map) { + return SearchDisplayFilters( + isNotInAlbum: map['isNotInAlbum'] as bool, + isArchive: map['isArchive'] as bool, + isFavorite: map['isFavorite'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory SearchDisplayFilters.fromJson(String source) => + SearchDisplayFilters.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'SearchDisplayFilters(isNotInAlbum: $isNotInAlbum, isArchive: $isArchive, isFavorite: $isFavorite)'; + + @override + bool operator ==(covariant SearchDisplayFilters other) { + if (identical(this, other)) return true; + + return other.isNotInAlbum == isNotInAlbum && + other.isArchive == isArchive && + other.isFavorite == isFavorite; + } + + @override + int get hashCode => + isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode; +} + +class SearchFilter { + String? context; + String? filename; + Set people; + SearchLocationFilter location; + SearchCameraFilter camera; + SearchDateFilter date; + SearchDisplayFilters display; + + // Enum + AssetType mediaType; + + SearchFilter({ + this.context, + this.filename, + required this.people, + required this.location, + required this.camera, + required this.date, + required this.display, + required this.mediaType, + }); + + SearchFilter copyWith({ + String? context, + String? filename, + Set? people, + SearchLocationFilter? location, + SearchCameraFilter? camera, + SearchDateFilter? date, + SearchDisplayFilters? display, + AssetType? mediaType, + }) { + return SearchFilter( + context: context ?? this.context, + filename: filename ?? this.filename, + people: people ?? this.people, + location: location ?? this.location, + camera: camera ?? this.camera, + date: date ?? this.date, + display: display ?? this.display, + mediaType: mediaType ?? this.mediaType, + ); + } + + @override + String toString() { + return 'SearchFilter(context: $context, filename: $filename, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)'; + } + + @override + bool operator ==(covariant SearchFilter other) { + if (identical(this, other)) return true; + + return other.context == context && + other.filename == filename && + other.people == people && + other.location == location && + other.camera == camera && + other.date == date && + other.display == display && + other.mediaType == mediaType; + } + + @override + int get hashCode { + return context.hashCode ^ + filename.hashCode ^ + people.hashCode ^ + location.hashCode ^ + camera.hashCode ^ + date.hashCode ^ + display.hashCode ^ + mediaType.hashCode; + } +} diff --git a/mobile/lib/modules/search/providers/paginated_search.provider.dart b/mobile/lib/modules/search/providers/paginated_search.provider.dart new file mode 100644 index 000000000..e20e37c52 --- /dev/null +++ b/mobile/lib/modules/search/providers/paginated_search.provider.dart @@ -0,0 +1,62 @@ +import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/services/search.service.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'paginated_search.provider.g.dart'; + +@riverpod +class PaginatedSearch extends _$PaginatedSearch { + Future?> _search(SearchFilter filter, int page) async { + final service = ref.read(searchServiceProvider); + final result = await service.search(filter, page); + + return result; + } + + @override + Future> build() async { + return []; + } + + Future> getNextPage(SearchFilter filter, int nextPage) async { + state = const AsyncValue.loading(); + + final newState = await AsyncValue.guard(() async { + final assets = await _search(filter, nextPage); + + if (assets != null) { + return [...?state.value, ...assets]; + } + }); + + state = newState.valueOrNull == null + ? const AsyncValue.data([]) + : AsyncValue.data(newState.value!); + + return newState.valueOrNull ?? []; + } + + clear() { + state = const AsyncValue.data([]); + } +} + +@riverpod +AsyncValue paginatedSearchRenderList( + PaginatedSearchRenderListRef ref, +) { + final assets = ref.watch(paginatedSearchProvider).value; + + if (assets != null) { + return ref.watch( + renderListProviderWithGrouping( + (assets, GroupAssetsBy.none), + ), + ); + } else { + return const AsyncValue.loading(); + } +} diff --git a/mobile/lib/modules/search/providers/paginated_search.provider.g.dart b/mobile/lib/modules/search/providers/paginated_search.provider.g.dart new file mode 100644 index 000000000..3357be777 --- /dev/null +++ b/mobile/lib/modules/search/providers/paginated_search.provider.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'paginated_search.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$paginatedSearchRenderListHash() => + r'c2cc2381ee6ea8f8e08d6d4c1289bbf0c6b9647e'; + +/// See also [paginatedSearchRenderList]. +@ProviderFor(paginatedSearchRenderList) +final paginatedSearchRenderListProvider = + AutoDisposeProvider>.internal( + paginatedSearchRenderList, + name: r'paginatedSearchRenderListProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$paginatedSearchRenderListHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PaginatedSearchRenderListRef + = AutoDisposeProviderRef>; +String _$paginatedSearchHash() => r'8312f358261368cf2b5572b839fdd8f8fbe9a62e'; + +/// See also [PaginatedSearch]. +@ProviderFor(PaginatedSearch) +final paginatedSearchProvider = + AutoDisposeAsyncNotifierProvider>.internal( + PaginatedSearch.new, + name: r'paginatedSearchProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$paginatedSearchHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PaginatedSearch = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/providers/people.provider.dart b/mobile/lib/modules/search/providers/people.provider.dart index 6009ee53a..398d1122a 100644 --- a/mobile/lib/modules/search/providers/people.provider.dart +++ b/mobile/lib/modules/search/providers/people.provider.dart @@ -1,51 +1,49 @@ -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/search/models/curated_content.dart'; -import 'package:immich_mobile/modules/search/services/person.service.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'people.provider.g.dart'; - -@riverpod -Future> getCuratedPeople( - GetCuratedPeopleRef ref, -) async { - final PersonService personService = ref.read(personServiceProvider); - - final curatedPeople = await personService.getCuratedPeople(); - - return curatedPeople - .map((p) => CuratedContent(id: p.id, label: p.name)) - .toList(); -} - -@riverpod -Future personAssets(PersonAssetsRef ref, String personId) async { - final PersonService personService = ref.read(personServiceProvider); - final assets = await personService.getPersonAssets(personId); - if (assets == null) { - return RenderList.empty(); - } - - final settings = ref.read(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - return await RenderList.fromAssets(assets, groupBy); -} - -@riverpod -Future updatePersonName( - UpdatePersonNameRef ref, - String personId, - String updatedName, -) async { - final PersonService personService = ref.read(personServiceProvider); - final person = await personService.updateName(personId, updatedName); - - if (person != null && person.name == updatedName) { - ref.invalidate(getCuratedPeopleProvider); - return true; - } - return false; -} +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/search/services/person.service.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'people.provider.g.dart'; + +@riverpod +Future> getAllPeople( + GetAllPeopleRef ref, +) async { + final PersonService personService = ref.read(personServiceProvider); + + final people = await personService.getAllPeople(); + + return people; +} + +@riverpod +Future personAssets(PersonAssetsRef ref, String personId) async { + final PersonService personService = ref.read(personServiceProvider); + final assets = await personService.getPersonAssets(personId); + if (assets == null) { + return RenderList.empty(); + } + + final settings = ref.read(appSettingsServiceProvider); + final groupBy = + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return await RenderList.fromAssets(assets, groupBy); +} + +@riverpod +Future updatePersonName( + UpdatePersonNameRef ref, + String personId, + String updatedName, +) async { + final PersonService personService = ref.read(personServiceProvider); + final person = await personService.updateName(personId, updatedName); + + if (person != null && person.name == updatedName) { + ref.invalidate(getAllPeopleProvider); + return true; + } + return false; +} diff --git a/mobile/lib/modules/search/providers/people.provider.g.dart b/mobile/lib/modules/search/providers/people.provider.g.dart index c13c2c160..c68f7a75f 100644 --- a/mobile/lib/modules/search/providers/people.provider.g.dart +++ b/mobile/lib/modules/search/providers/people.provider.g.dart @@ -6,23 +6,21 @@ part of 'people.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0'; +String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd'; -/// See also [getCuratedPeople]. -@ProviderFor(getCuratedPeople) -final getCuratedPeopleProvider = - AutoDisposeFutureProvider>.internal( - getCuratedPeople, - name: r'getCuratedPeopleProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$getCuratedPeopleHash, +/// See also [getAllPeople]. +@ProviderFor(getAllPeople) +final getAllPeopleProvider = + AutoDisposeFutureProvider>.internal( + getAllPeople, + name: r'getAllPeopleProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$getAllPeopleHash, dependencies: null, allTransitiveDependencies: null, ); -typedef GetCuratedPeopleRef - = AutoDisposeFutureProviderRef>; +typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d'; /// Copied from Dart SDK @@ -172,7 +170,7 @@ class _PersonAssetsProviderElement String get personId => (origin as PersonAssetsProvider).personId; } -String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd'; +String _$updatePersonNameHash() => r'7145aaaf6fc38fdafe3a283ebf3d3f4fd0774cd2'; /// See also [updatePersonName]. @ProviderFor(updatePersonName) diff --git a/mobile/lib/modules/search/providers/search_filter.provider.dart b/mobile/lib/modules/search/providers/search_filter.provider.dart new file mode 100644 index 000000000..1a4914b41 --- /dev/null +++ b/mobile/lib/modules/search/providers/search_filter.provider.dart @@ -0,0 +1,27 @@ +import 'package:immich_mobile/modules/search/services/search.service.dart'; +import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'search_filter.provider.g.dart'; + +@riverpod +Future> getSearchSuggestions( + GetSearchSuggestionsRef ref, + SearchSuggestionType type, { + String? locationCountry, + String? locationState, + String? make, + String? model, +}) async { + final SearchService service = ref.read(searchServiceProvider); + + final suggestions = await service.getSearchSuggestions( + type, + country: locationCountry, + state: locationState, + make: make, + model: model, + ); + + return suggestions ?? []; +} diff --git a/mobile/lib/modules/search/providers/search_filter.provider.g.dart b/mobile/lib/modules/search/providers/search_filter.provider.g.dart new file mode 100644 index 000000000..d5cdaa031 --- /dev/null +++ b/mobile/lib/modules/search/providers/search_filter.provider.g.dart @@ -0,0 +1,229 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'search_filter.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getSearchSuggestionsHash() => + r'bc1e9a1a060868f14e6eb970d2251dbfe39c6866'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [getSearchSuggestions]. +@ProviderFor(getSearchSuggestions) +const getSearchSuggestionsProvider = GetSearchSuggestionsFamily(); + +/// See also [getSearchSuggestions]. +class GetSearchSuggestionsFamily extends Family>> { + /// See also [getSearchSuggestions]. + const GetSearchSuggestionsFamily(); + + /// See also [getSearchSuggestions]. + GetSearchSuggestionsProvider call( + SearchSuggestionType type, { + String? locationCountry, + String? locationState, + String? make, + String? model, + }) { + return GetSearchSuggestionsProvider( + type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ); + } + + @override + GetSearchSuggestionsProvider getProviderOverride( + covariant GetSearchSuggestionsProvider provider, + ) { + return call( + provider.type, + locationCountry: provider.locationCountry, + locationState: provider.locationState, + make: provider.make, + model: provider.model, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'getSearchSuggestionsProvider'; +} + +/// See also [getSearchSuggestions]. +class GetSearchSuggestionsProvider + extends AutoDisposeFutureProvider> { + /// See also [getSearchSuggestions]. + GetSearchSuggestionsProvider( + SearchSuggestionType type, { + String? locationCountry, + String? locationState, + String? make, + String? model, + }) : this._internal( + (ref) => getSearchSuggestions( + ref as GetSearchSuggestionsRef, + type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ), + from: getSearchSuggestionsProvider, + name: r'getSearchSuggestionsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getSearchSuggestionsHash, + dependencies: GetSearchSuggestionsFamily._dependencies, + allTransitiveDependencies: + GetSearchSuggestionsFamily._allTransitiveDependencies, + type: type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ); + + GetSearchSuggestionsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.type, + required this.locationCountry, + required this.locationState, + required this.make, + required this.model, + }) : super.internal(); + + final SearchSuggestionType type; + final String? locationCountry; + final String? locationState; + final String? make; + final String? model; + + @override + Override overrideWith( + FutureOr> Function(GetSearchSuggestionsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: GetSearchSuggestionsProvider._internal( + (ref) => create(ref as GetSearchSuggestionsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + type: type, + locationCountry: locationCountry, + locationState: locationState, + make: make, + model: model, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _GetSearchSuggestionsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is GetSearchSuggestionsProvider && + other.type == type && + other.locationCountry == locationCountry && + other.locationState == locationState && + other.make == make && + other.model == model; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, type.hashCode); + hash = _SystemHash.combine(hash, locationCountry.hashCode); + hash = _SystemHash.combine(hash, locationState.hashCode); + hash = _SystemHash.combine(hash, make.hashCode); + hash = _SystemHash.combine(hash, model.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin GetSearchSuggestionsRef on AutoDisposeFutureProviderRef> { + /// The parameter `type` of this provider. + SearchSuggestionType get type; + + /// The parameter `locationCountry` of this provider. + String? get locationCountry; + + /// The parameter `locationState` of this provider. + String? get locationState; + + /// The parameter `make` of this provider. + String? get make; + + /// The parameter `model` of this provider. + String? get model; +} + +class _GetSearchSuggestionsProviderElement + extends AutoDisposeFutureProviderElement> + with GetSearchSuggestionsRef { + _GetSearchSuggestionsProviderElement(super.provider); + + @override + SearchSuggestionType get type => + (origin as GetSearchSuggestionsProvider).type; + @override + String? get locationCountry => + (origin as GetSearchSuggestionsProvider).locationCountry; + @override + String? get locationState => + (origin as GetSearchSuggestionsProvider).locationState; + @override + String? get make => (origin as GetSearchSuggestionsProvider).make; + @override + String? get model => (origin as GetSearchSuggestionsProvider).model; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart deleted file mode 100644 index e220cc69f..000000000 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; - -import 'package:immich_mobile/modules/search/services/search.service.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; - -class SearchResultPageNotifier extends StateNotifier { - SearchResultPageNotifier(this._searchService) - : super( - SearchResultPageState( - searchResult: [], - isError: false, - isLoading: true, - isSuccess: false, - isSmart: false, - ), - ); - - final SearchService _searchService; - - Future search(String searchTerm, {bool smartSearch = true}) async { - state = state.copyWith( - searchResult: [], - isError: false, - isLoading: true, - isSuccess: false, - ); - - List? assets = - await _searchService.searchAsset(searchTerm, smartSearch: smartSearch); - - if (assets != null) { - state = state.copyWith( - searchResult: assets, - isError: false, - isLoading: false, - isSuccess: true, - isSmart: smartSearch, - ); - } else { - state = state.copyWith( - searchResult: [], - isError: true, - isLoading: false, - isSuccess: false, - isSmart: smartSearch, - ); - } - } -} - -final searchResultPageProvider = - StateNotifierProvider( - (ref) { - return SearchResultPageNotifier(ref.watch(searchServiceProvider)); -}); - -final searchRenderListProvider = Provider((ref) { - final result = ref.watch(searchResultPageProvider); - return ref.watch( - renderListProviderWithGrouping( - (result.searchResult, result.isSmart ? GroupAssetsBy.none : null), - ), - ); -}); diff --git a/mobile/lib/modules/search/services/person.service.dart b/mobile/lib/modules/search/services/person.service.dart index 4f92e729f..884a01c9f 100644 --- a/mobile/lib/modules/search/services/person.service.dart +++ b/mobile/lib/modules/search/services/person.service.dart @@ -20,7 +20,7 @@ class PersonService { PersonService(this._apiService, this._db); - Future> getCuratedPeople() async { + Future> getAllPeople() async { try { final peopleResponseDto = await _apiService.personApi.getAllPeople(); return peopleResponseDto?.people ?? []; diff --git a/mobile/lib/modules/search/services/search.service.dart b/mobile/lib/modules/search/services/search.service.dart index 35249dec5..4d19657af 100644 --- a/mobile/lib/modules/search/services/search.service.dart +++ b/mobile/lib/modules/search/services/search.service.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; @@ -29,25 +30,92 @@ class SearchService { } } - Future?> searchAsset( - String searchTerm, { - bool smartSearch = true, + Future?> getSearchSuggestions( + SearchSuggestionType type, { + String? country, + String? state, + String? make, + String? model, }) async { - // TODO search in local DB: 1. when offline, 2. to find local assets try { - final SearchResponseDto? results = await _apiService.searchApi.search( - query: searchTerm, - smart: smartSearch, + return await _apiService.searchApi.getSearchSuggestions( + type, + country: country, + state: state, + make: make, + model: model, ); - if (results == null) { + } catch (e) { + debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}"); + return []; + } + } + + Future?> search(SearchFilter filter, int page) async { + try { + SearchResponseDto? response; + AssetTypeEnum? type; + if (filter.mediaType == AssetType.image) { + type = AssetTypeEnum.IMAGE; + } else if (filter.mediaType == AssetType.video) { + type = AssetTypeEnum.VIDEO; + } + + if (filter.context != null && filter.context!.isNotEmpty) { + response = await _apiService.searchApi.searchSmart( + SmartSearchDto( + query: filter.context!, + country: filter.location.country, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + isArchived: filter.display.isArchive, + isFavorite: filter.display.isFavorite, + isNotInAlbum: filter.display.isNotInAlbum, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } else { + response = await _apiService.searchApi.searchMetadata( + MetadataSearchDto( + originalFileName: + filter.filename != null && filter.filename!.isNotEmpty + ? filter.filename + : null, + country: filter.location.country, + state: filter.location.state, + city: filter.location.city, + make: filter.camera.make, + model: filter.camera.model, + takenAfter: filter.date.takenAfter, + takenBefore: filter.date.takenBefore, + isArchived: filter.display.isArchive, + isFavorite: filter.display.isFavorite, + isNotInAlbum: filter.display.isNotInAlbum, + personIds: filter.people.map((e) => e.id).toList(), + type: type, + page: page, + size: 1000, + ), + ); + } + + if (response == null) { return null; } - // TODO local DB might be out of date; add assets not yet in DB? - return _db.assets.getAllByRemoteId(results.assets.items.map((e) => e.id)); - } catch (e) { - debugPrint("[ERROR] [searchAsset] ${e.toString()}"); - return null; + + return _db.assets + .getAllByRemoteId(response.assets.items.map((e) => e.id)); + } catch (error) { + debugPrint("Error [search] $error"); } + return null; } Future?> getCuratedLocation() async { diff --git a/mobile/lib/modules/search/ui/explore_grid.dart b/mobile/lib/modules/search/ui/explore_grid.dart index fd49fff7c..ba55b5581 100644 --- a/mobile/lib/modules/search/ui/explore_grid.dart +++ b/mobile/lib/modules/search/ui/explore_grid.dart @@ -1,8 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -57,7 +59,22 @@ class ExploreGrid extends StatelessWidget { ), ) : context.pushRoute( - SearchResultRoute(searchTerm: 'm:${content.label}'), + SearchInputRoute( + prefilter: SearchFilter( + people: {}, + location: SearchLocationFilter( + city: content.label, + ), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: AssetType.other, + ), + ), ); }, ); diff --git a/mobile/lib/modules/search/ui/immich_search_bar.dart b/mobile/lib/modules/search/ui/immich_search_bar.dart deleted file mode 100644 index f4fa62d26..000000000 --- a/mobile/lib/modules/search/ui/immich_search_bar.dart +++ /dev/null @@ -1,99 +0,0 @@ -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/modules/search/providers/search_page_state.provider.dart'; - -class ImmichSearchBar extends HookConsumerWidget - implements PreferredSizeWidget { - const ImmichSearchBar({ - super.key, - required this.searchFocusNode, - required this.onSubmitted, - }); - - final FocusNode searchFocusNode; - final Function(String) onSubmitted; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTermController = useTextEditingController(text: ""); - final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; - - focusSearch() { - searchTermController.clear(); - ref.watch(searchPageStateProvider.notifier).getSuggestedSearchTerms(); - ref.watch(searchPageStateProvider.notifier).enableSearch(); - ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - - searchFocusNode.requestFocus(); - } - - useEffect( - () { - searchFocusNotifier.addListener(focusSearch); - return () { - searchFocusNotifier.removeListener(focusSearch); - }; - }, - [], - ); - - return AppBar( - automaticallyImplyLeading: false, - leading: isSearchEnabled - ? IconButton( - onPressed: () { - searchFocusNode.unfocus(); - ref.watch(searchPageStateProvider.notifier).disableSearch(); - searchTermController.clear(); - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ) - : const Icon( - Icons.search_rounded, - size: 20, - ), - title: TextField( - controller: searchTermController, - focusNode: searchFocusNode, - autofocus: false, - onTap: focusSearch, - onSubmitted: (searchTerm) { - onSubmitted(searchTerm); - searchTermController.clear(); - ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - }, - onChanged: (value) { - ref.watch(searchPageStateProvider.notifier).setSearchTerm(value); - }, - decoration: InputDecoration( - hintText: 'search_bar_hint'.tr(), - hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurface.withOpacity(0.75), - ), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - ), - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -// Used to focus search from outside this widget. -// For example when double pressing the search nav icon. -final searchFocusNotifier = SearchFocusNotifier(); - -class SearchFocusNotifier with ChangeNotifier { - void requestFocus() { - notifyListeners(); - } -} diff --git a/mobile/lib/modules/search/ui/search_filter/camera_picker.dart b/mobile/lib/modules/search/ui/search_filter/camera_picker.dart new file mode 100644 index 000000000..fdfd398e6 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/camera_picker.dart @@ -0,0 +1,120 @@ +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/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart'; +import 'package:openapi/api.dart'; + +class CameraPicker extends HookConsumerWidget { + const CameraPicker({super.key, required this.onSelect, this.filter}); + + final Function(Map) onSelect; + final SearchCameraFilter? filter; + @override + Widget build(BuildContext context, WidgetRef ref) { + final makeTextController = useTextEditingController(text: filter?.make); + final modelTextController = useTextEditingController(text: filter?.model); + final selectedMake = useState(filter?.make); + final selectedModel = useState(filter?.model); + + final make = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.cameraMake, + ), + ); + + final models = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.cameraModel, + make: selectedMake.value, + ), + ); + + final inputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + ), + contentPadding: const EdgeInsets.only(left: 16), + ); + + final menuStyle = MenuStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ); + + return Container( + padding: const EdgeInsets.only( + // bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + DropdownMenu( + dropdownMenuEntries: switch (make) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + width: context.width * 0.45, + menuHeight: 400, + label: const Text('Make'), + inputDecorationTheme: inputDecorationTheme, + controller: makeTextController, + menuStyle: menuStyle, + leadingIcon: const Icon(Icons.photo_camera_rounded), + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedMake.value = value.toString(); + onSelect({ + 'make': selectedMake.value, + 'model': selectedModel.value, + }); + }, + ), + DropdownMenu( + dropdownMenuEntries: switch (models) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + width: context.width * 0.45, + menuHeight: 400, + label: const Text('Model'), + inputDecorationTheme: inputDecorationTheme, + controller: modelTextController, + menuStyle: menuStyle, + leadingIcon: const Icon(Icons.camera), + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedModel.value = value.toString(); + onSelect({ + 'make': selectedMake.value, + 'model': selectedModel.value, + }); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/display_option_picker.dart b/mobile/lib/modules/search/ui/search_filter/display_option_picker.dart new file mode 100644 index 000000000..f6cd01cbb --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/display_option_picker.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; + +enum DisplayOption { + notInAlbum, + favorite, + archive, +} + +class DisplayOptionPicker extends HookWidget { + const DisplayOptionPicker({ + super.key, + required this.onSelect, + this.filter, + }); + + final Function(Map) onSelect; + final SearchDisplayFilters? filter; + + @override + Widget build(BuildContext context) { + final options = useState>({ + DisplayOption.notInAlbum: filter?.isNotInAlbum ?? false, + DisplayOption.favorite: filter?.isFavorite ?? false, + DisplayOption.archive: filter?.isArchive ?? false, + }); + + return ListView( + shrinkWrap: true, + children: [ + CheckboxListTile( + title: const Text('Not in album'), + value: options.value[DisplayOption.notInAlbum], + onChanged: (bool? value) { + options.value = { + ...options.value, + DisplayOption.notInAlbum: value!, + }; + onSelect(options.value); + }, + ), + CheckboxListTile( + title: const Text('Favorite'), + value: options.value[DisplayOption.favorite], + onChanged: (value) { + options.value = { + ...options.value, + DisplayOption.favorite: value!, + }; + onSelect(options.value); + }, + ), + CheckboxListTile( + title: const Text('Archive'), + value: options.value[DisplayOption.archive], + onChanged: (value) { + options.value = { + ...options.value, + DisplayOption.archive: value!, + }; + onSelect(options.value); + }, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart b/mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart new file mode 100644 index 000000000..46bfe96bb --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class FilterBottomSheetScaffold extends StatelessWidget { + const FilterBottomSheetScaffold({ + super.key, + required this.child, + required this.onSearch, + required this.onClear, + required this.title, + this.expanded, + }); + + final bool? expanded; + final String title; + final Widget child; + final Function() onSearch; + final Function() onClear; + + @override + Widget build(BuildContext context) { + buildChildWidget() { + if (expanded != null && expanded == true) { + return Expanded(child: child); + } + return child; + } + + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: context.textTheme.headlineSmall, + ), + ), + buildChildWidget(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () { + onClear(); + Navigator.of(context).pop(); + }, + child: const Text('Clear'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () { + onSearch(); + Navigator.of(context).pop(); + }, + child: const Text('Apply filter'), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/location_picker.dart b/mobile/lib/modules/search/ui/search_filter/location_picker.dart new file mode 100644 index 000000000..22568da47 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/location_picker.dart @@ -0,0 +1,166 @@ +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/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/search_filter.provider.dart'; +import 'package:openapi/api.dart'; + +class LocationPicker extends HookConsumerWidget { + const LocationPicker({super.key, required this.onSelected, this.filter}); + + final Function(Map) onSelected; + final SearchLocationFilter? filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final countryTextController = + useTextEditingController(text: filter?.country); + final stateTextController = useTextEditingController(text: filter?.state); + final cityTextController = useTextEditingController(text: filter?.city); + + final selectedCountry = useState(filter?.country); + final selectedState = useState(filter?.state); + final selectedCity = useState(filter?.city); + + final countries = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.country, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), + ); + + final states = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.state, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), + ); + + final cities = ref.watch( + getSearchSuggestionsProvider( + SearchSuggestionType.city, + locationCountry: selectedCountry.value, + locationState: selectedState.value, + ), + ); + + final inputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + ), + contentPadding: const EdgeInsets.only(left: 16), + ); + + final menuStyle = MenuStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ); + + return Column( + children: [ + DropdownMenu( + dropdownMenuEntries: switch (countries) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + menuHeight: 400, + width: context.width * 0.9, + label: const Text('Country'), + inputDecorationTheme: inputDecorationTheme, + menuStyle: menuStyle, + controller: countryTextController, + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedCountry.value = value.toString(); + onSelected({ + 'country': selectedCountry.value, + 'state': selectedState.value, + 'city': selectedCity.value, + }); + }, + ), + const SizedBox( + height: 16, + ), + DropdownMenu( + dropdownMenuEntries: switch (states) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + menuHeight: 400, + width: context.width * 0.9, + label: const Text('State'), + inputDecorationTheme: inputDecorationTheme, + menuStyle: menuStyle, + controller: stateTextController, + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedState.value = value.toString(); + onSelected({ + 'country': selectedCountry.value, + 'state': selectedState.value, + 'city': selectedCity.value, + }); + }, + ), + const SizedBox( + height: 16, + ), + DropdownMenu( + dropdownMenuEntries: switch (cities) { + AsyncError() => [], + AsyncData(:final value) => value + .map( + (e) => DropdownMenuEntry( + value: e, + label: e, + ), + ) + .toList(), + _ => [], + }, + menuHeight: 400, + width: context.width * 0.9, + label: const Text('City'), + inputDecorationTheme: inputDecorationTheme, + menuStyle: menuStyle, + controller: cityTextController, + trailingIcon: const Icon(Icons.arrow_drop_down_rounded), + selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded), + onSelected: (value) { + selectedCity.value = value.toString(); + onSelected({ + 'country': selectedCountry.value, + 'state': selectedState.value, + 'city': selectedCity.value, + }); + }, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/media_type_picker.dart b/mobile/lib/modules/search/ui/search_filter/media_type_picker.dart new file mode 100644 index 000000000..aaef2c815 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/media_type_picker.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; + +class MediaTypePicker extends HookWidget { + const MediaTypePicker({super.key, required this.onSelect, this.filter}); + + final Function(AssetType) onSelect; + final AssetType? filter; + + @override + Widget build(BuildContext context) { + final selectedMediaType = useState(filter ?? AssetType.other); + + return ListView( + shrinkWrap: true, + children: [ + RadioListTile( + title: const Text("All"), + value: AssetType.other, + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + ), + RadioListTile( + title: const Text("Image"), + value: AssetType.image, + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + ), + RadioListTile( + title: const Text("Video"), + value: AssetType.video, + onChanged: (value) { + selectedMediaType.value = value!; + onSelect(value); + }, + groupValue: selectedMediaType.value, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/people_picker.dart b/mobile/lib/modules/search/ui/search_filter/people_picker.dart new file mode 100644 index 000000000..74aad06b8 --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/people_picker.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/providers/people.provider.dart'; +import 'package:immich_mobile/shared/models/store.dart' as local_store; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:openapi/api.dart'; + +class PeoplePicker extends HookConsumerWidget { + const PeoplePicker({super.key, required this.onSelect, this.filter}); + + final Function(Set) onSelect; + final Set? filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + var imageSize = 45.0; + final people = ref.watch(getAllPeopleProvider); + final headers = { + "x-immich-user-token": + local_store.Store.get(local_store.StoreKey.accessToken), + }; + final selectedPeople = useState>(filter ?? {}); + + return people.widgetWhen( + onData: (people) { + return ListView.builder( + shrinkWrap: true, + itemCount: people.length, + padding: const EdgeInsets.all(8), + itemBuilder: (context, index) { + final person = people[index]; + return Card( + elevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + child: ListTile( + title: Text( + person.name, + style: context.textTheme.bodyLarge, + ), + leading: SizedBox( + height: imageSize, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + maxRadius: imageSize / 2, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: headers, + ), + ), + ), + ), + onTap: () { + if (selectedPeople.value.contains(person)) { + selectedPeople.value.remove(person); + } else { + selectedPeople.value.add(person); + } + + selectedPeople.value = {...selectedPeople.value}; + onSelect(selectedPeople.value); + }, + selected: selectedPeople.value.contains(person), + selectedTileColor: context.primaryColor.withOpacity(0.2), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart b/mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart new file mode 100644 index 000000000..b2e0d086a --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/search_filter_chip.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SearchFilterChip extends StatelessWidget { + final String label; + final Function() onTap; + final Widget? currentFilter; + final IconData icon; + + const SearchFilterChip({ + super.key, + required this.label, + required this.onTap, + required this.icon, + this.currentFilter, + }); + + @override + Widget build(BuildContext context) { + if (currentFilter != null) { + return GestureDetector( + onTap: onTap, + child: Card( + elevation: 0, + color: context.primaryColor.withAlpha(25), + shape: StadiumBorder( + side: BorderSide(color: context.primaryColor), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), + child: Row( + children: [ + Icon( + icon, + size: 18, + ), + const SizedBox(width: 4.0), + currentFilter!, + ], + ), + ), + ), + ); + } + return GestureDetector( + onTap: onTap, + child: Card( + elevation: 0, + shape: + StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), + child: Row( + children: [ + Icon( + icon, + size: 18, + ), + const SizedBox(width: 4.0), + Text(label), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart b/mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart new file mode 100644 index 000000000..57545413d --- /dev/null +++ b/mobile/lib/modules/search/ui/search_filter/search_filter_utils.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +Future showFilterBottomSheet({ + required BuildContext context, + required Widget child, + bool isScrollControlled = false, + bool isDismissible = true, +}) async { + return await showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + useSafeArea: false, + isDismissible: isDismissible, + showDragHandle: isDismissible, + builder: (BuildContext context) { + return child; + }, + ); +} diff --git a/mobile/lib/modules/search/ui/search_suggestion_list.dart b/mobile/lib/modules/search/ui/search_suggestion_list.dart deleted file mode 100644 index c9694eb75..000000000 --- a/mobile/lib/modules/search/ui/search_suggestion_list.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; - -class SearchSuggestionList extends ConsumerWidget { - const SearchSuggestionList({super.key, required this.onSubmitted}); - - final Function(String) onSubmitted; - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTerm = ref.watch(searchPageStateProvider).searchTerm; - final searchSuggestion = - ref.watch(searchPageStateProvider).searchSuggestion; - - return Container( - color: searchTerm.isEmpty - ? Colors.black.withOpacity(0.5) - : context.scaffoldBackgroundColor, - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Container( - color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[100], - child: Padding( - padding: const EdgeInsets.all(16.0), - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'search_suggestion_list_smart_search_hint_1'.tr(), - style: context.textTheme.bodyMedium, - ), - TextSpan( - text: 'search_suggestion_list_smart_search_hint_2'.tr(), - style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - ), - ), - SliverFillRemaining( - hasScrollBody: true, - child: ListView.builder( - itemBuilder: ((context, index) { - return ListTile( - onTap: () { - onSubmitted("m:${searchSuggestion[index]}"); - }, - title: Text(searchSuggestion[index]), - ); - }), - itemCount: searchSuggestion.length, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart index 1f90922c1..3414edc05 100644 --- a/mobile/lib/modules/search/views/all_people_page.dart +++ b/mobile/lib/modules/search/views/all_people_page.dart @@ -1,35 +1,38 @@ -import 'package:auto_route/auto_route.dart'; -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/modules/search/providers/people.provider.dart'; -import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; - -@RoutePage() -class AllPeoplePage extends HookConsumerWidget { - const AllPeoplePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final curatedPeople = ref.watch(getCuratedPeopleProvider); - - return Scaffold( - appBar: AppBar( - title: const Text( - 'all_people_page_title', - ).tr(), - leading: IconButton( - onPressed: () => context.popRoute(), - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - ), - body: curatedPeople.widgetWhen( - onData: (people) => ExploreGrid( - isPeople: true, - curatedContent: people, - ), - ), - ); - } -} +import 'package:auto_route/auto_route.dart'; +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/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/providers/people.provider.dart'; +import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; + +@RoutePage() +class AllPeoplePage extends HookConsumerWidget { + const AllPeoplePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final curatedPeople = ref.watch(getAllPeopleProvider); + + return Scaffold( + appBar: AppBar( + title: const Text( + 'all_people_page_title', + ).tr(), + leading: IconButton( + onPressed: () => context.popRoute(), + icon: const Icon(Icons.arrow_back_ios_rounded), + ), + ), + body: curatedPeople.widgetWhen( + onData: (people) => ExploreGrid( + isPeople: true, + curatedContent: people + .map((e) => CuratedContent(label: e.name, id: e.id)) + .toList(), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_input_page.dart b/mobile/lib/modules/search/views/search_input_page.dart new file mode 100644 index 000000000..a35341606 --- /dev/null +++ b/mobile/lib/modules/search/views/search_input_page.dart @@ -0,0 +1,563 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.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/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/paginated_search.provider.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/camera_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/display_option_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/filter_bottom_sheet_scaffold.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/location_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/media_type_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/people_picker.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_chip.dart'; +import 'package:immich_mobile/modules/search/ui/search_filter/search_filter_utils.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; +import 'package:openapi/api.dart'; + +@RoutePage() +class SearchInputPage extends HookConsumerWidget { + const SearchInputPage({super.key, this.prefilter}); + + final SearchFilter? prefilter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isContextualSearch = useState(true); + final textSearchController = useTextEditingController(); + final filter = useState( + SearchFilter( + people: prefilter?.people ?? {}, + location: prefilter?.location ?? SearchLocationFilter(), + camera: prefilter?.camera ?? SearchCameraFilter(), + date: prefilter?.date ?? SearchDateFilter(), + display: prefilter?.display ?? + SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: prefilter?.mediaType ?? AssetType.other, + ), + ); + + final previousFilter = useState(filter.value); + + final peopleCurrentFilterWidget = useState(null); + final dateRangeCurrentFilterWidget = useState(null); + final cameraCurrentFilterWidget = useState(null); + final locationCurrentFilterWidget = useState(null); + final mediaTypeCurrentFilterWidget = useState(null); + final displayOptionCurrentFilterWidget = useState(null); + + final currentPage = useState(1); + final searchProvider = ref.watch(paginatedSearchProvider); + final searchResultCount = useState(0); + + search() async { + if (prefilter == null && filter.value == previousFilter.value) return; + + ref.watch(paginatedSearchProvider.notifier).clear(); + + currentPage.value = 1; + + final searchResult = await ref + .watch(paginatedSearchProvider.notifier) + .getNextPage(filter.value, currentPage.value); + previousFilter.value = filter.value; + + searchResultCount.value = searchResult.length; + } + + searchPrefilter() { + if (prefilter != null) { + Future.delayed( + Duration.zero, + () { + search(); + + if (prefilter!.location.city != null) { + locationCurrentFilterWidget.value = Text( + prefilter!.location.city!, + style: context.textTheme.labelLarge, + ); + } + }, + ); + } + } + + useEffect( + () { + searchPrefilter(); + return null; + }, + [], + ); + + loadMoreSearchResult() async { + currentPage.value += 1; + final searchResult = await ref + .watch(paginatedSearchProvider.notifier) + .getNextPage(filter.value, currentPage.value); + searchResultCount.value = searchResult.length; + } + + showPeoplePicker() { + handleOnSelect(Set value) { + filter.value = filter.value.copyWith( + people: value, + ); + + peopleCurrentFilterWidget.value = Text( + value.map((e) => e.name != '' ? e.name : "No name").join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + people: {}, + ); + + peopleCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'Select people', + expanded: true, + onSearch: search, + onClear: handleClear, + child: PeoplePicker( + onSelect: handleOnSelect, + filter: filter.value.people, + ), + ), + ), + ); + } + + showLocationPicker() { + handleOnSelect(Map value) { + filter.value = filter.value.copyWith( + location: SearchLocationFilter( + country: value['country'], + city: value['city'], + state: value['state'], + ), + ); + + final locationText = []; + if (value['country'] != null) { + locationText.add(value['country']!); + } + + if (value['state'] != null) { + locationText.add(value['state']!); + } + + if (value['city'] != null) { + locationText.add(value['city']!); + } + + locationCurrentFilterWidget.value = Text( + locationText.join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + location: SearchLocationFilter(), + ); + + locationCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + child: FilterBottomSheetScaffold( + title: 'Select location', + onSearch: search, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: LocationPicker( + onSelected: handleOnSelect, + filter: filter.value.location, + ), + ), + ), + ), + ); + } + + showCameraPicker() { + handleOnSelect(Map value) { + filter.value = filter.value.copyWith( + camera: SearchCameraFilter( + make: value['make'], + model: value['model'], + ), + ); + + cameraCurrentFilterWidget.value = Text( + '${value['make'] ?? ''} ${value['model'] ?? ''}', + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + camera: SearchCameraFilter(), + ); + + cameraCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + child: FilterBottomSheetScaffold( + title: 'Select camera type', + onSearch: search, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: CameraPicker( + onSelect: handleOnSelect, + filter: filter.value.camera, + ), + ), + ), + ); + } + + showDatePicker() async { + final firstDate = DateTime(1900); + final lastDate = DateTime.now(); + + final date = await showDateRangePicker( + context: context, + firstDate: firstDate, + lastDate: lastDate, + currentDate: DateTime.now(), + initialDateRange: DateTimeRange( + start: filter.value.date.takenAfter ?? lastDate, + end: filter.value.date.takenBefore ?? lastDate, + ), + helpText: 'Select a date range', + cancelText: 'Cancel', + confirmText: 'Select', + saveText: 'Save', + errorFormatText: 'Invalid date format', + errorInvalidText: 'Invalid date', + fieldStartHintText: 'Start date', + fieldEndHintText: 'End date', + initialEntryMode: DatePickerEntryMode.input, + ); + + if (date == null) { + filter.value = filter.value.copyWith( + date: SearchDateFilter(), + ); + + dateRangeCurrentFilterWidget.value = null; + search(); + return; + } + + filter.value = filter.value.copyWith( + date: SearchDateFilter( + takenAfter: date.start, + takenBefore: date.end.add( + const Duration( + hours: 23, + minutes: 59, + seconds: 59, + ), + ), + ), + ); + + // If date range is less than 24 hours, set the end date to the end of the day + if (date.end.difference(date.start).inHours < 24) { + dateRangeCurrentFilterWidget.value = Text( + date.start.toLocal().toIso8601String().split('T').first, + style: context.textTheme.labelLarge, + ); + } else { + dateRangeCurrentFilterWidget.value = Text( + '${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}', + style: context.textTheme.labelLarge, + ); + } + + search(); + } + + // MEDIA PICKER + showMediaTypePicker() { + handleOnSelected(AssetType assetType) { + filter.value = filter.value.copyWith( + mediaType: assetType, + ); + + mediaTypeCurrentFilterWidget.value = Text( + assetType == AssetType.image ? 'Image' : 'Video', + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + mediaType: AssetType.other, + ); + + mediaTypeCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'Select media type', + onSearch: search, + onClear: handleClear, + child: MediaTypePicker( + onSelect: handleOnSelected, + filter: filter.value.mediaType, + ), + ), + ); + } + + // DISPLAY OPTION + showDisplayOptionPicker() { + handleOnSelect(Map value) { + final filterText = []; + + value.forEach((key, value) { + switch (key) { + case DisplayOption.notInAlbum: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isNotInAlbum: value, + ), + ); + if (value) filterText.add('Not in album'); + break; + case DisplayOption.archive: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isArchive: value, + ), + ); + if (value) filterText.add('Archive'); + break; + case DisplayOption.favorite: + filter.value = filter.value.copyWith( + display: filter.value.display.copyWith( + isFavorite: value, + ), + ); + if (value) filterText.add('Favorite'); + break; + } + }); + + displayOptionCurrentFilterWidget.value = Text( + filterText.join(', '), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith( + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + ); + + displayOptionCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'Display options', + onSearch: search, + onClear: handleClear, + child: DisplayOptionPicker( + onSelect: handleOnSelect, + filter: filter.value.display, + ), + ), + ); + } + + handleTextSubmitted(String value) { + if (isContextualSearch.value) { + filter.value = filter.value.copyWith( + context: value, + filename: null, + ); + } else { + filter.value = filter.value.copyWith(filename: value, context: null); + } + + search(); + } + + buildSearchResult() { + return switch (searchProvider) { + AsyncData() => Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + final shouldLoadMore = searchResultCount.value > 75; + if (metrics.pixels >= metrics.maxScrollExtent && + shouldLoadMore) { + loadMoreSearchResult(); + } + return true; + }, + child: MultiselectGrid( + renderListProvider: paginatedSearchRenderListProvider, + archiveEnabled: true, + deleteEnabled: true, + editEnabled: true, + favoriteEnabled: true, + stackEnabled: false, + emptyIndicator: const SizedBox(), + ), + ), + ), + ), + AsyncError(:final error) => Text('Error: $error'), + _ => const Expanded(child: Center(child: CircularProgressIndicator())), + }; + } + + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + automaticallyImplyLeading: true, + actions: [ + IconButton( + icon: isContextualSearch.value + ? const Icon(Icons.abc_rounded) + : const Icon(Icons.image_search_rounded), + onPressed: () { + isContextualSearch.value = !isContextualSearch.value; + textSearchController.clear(); + }, + ), + ], + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded), + onPressed: () { + context.router.pop(); + }, + ), + title: TextField( + controller: textSearchController, + decoration: InputDecoration( + hintText: isContextualSearch.value + ? 'Sunrise on the beach' + : 'File name or extension', + hintStyle: context.textTheme.bodyLarge?.copyWith( + color: context.themeData.colorScheme.onSurface.withOpacity(0.75), + fontWeight: FontWeight.w500, + ), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + ), + onSubmitted: handleTextSubmitted, + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: SizedBox( + height: 50, + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SearchFilterChip( + icon: Icons.people_alt_rounded, + onTap: showPeoplePicker, + label: 'People', + currentFilter: peopleCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.location_pin, + onTap: showLocationPicker, + label: 'Location', + currentFilter: locationCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.camera_alt_rounded, + onTap: showCameraPicker, + label: 'Camera', + currentFilter: cameraCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.date_range_rounded, + onTap: showDatePicker, + label: 'Date', + currentFilter: dateRangeCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.video_collection_outlined, + onTap: showMediaTypePicker, + label: 'Media Type', + currentFilter: mediaTypeCurrentFilterWidget.value, + ), + SearchFilterChip( + icon: Icons.display_settings_outlined, + onTap: showDisplayOptionPicker, + label: 'Display Options', + currentFilter: displayOptionCurrentFilterWidget.value, + ), + ], + ), + ), + ), + buildSearchResult(), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index ab114d691..27ca28126 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -1,279 +1,274 @@ -import 'dart:math' as math; -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' hide Store; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/search/models/curated_content.dart'; -import 'package:immich_mobile/modules/search/providers/people.provider.dart'; -import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; -import 'package:immich_mobile/modules/search/ui/curated_people_row.dart'; -import 'package:immich_mobile/modules/search/ui/curated_places_row.dart'; -import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart'; -import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; -import 'package:immich_mobile/modules/search/ui/search_row_title.dart'; -import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; - -@RoutePage() -// ignore: must_be_immutable -class SearchPage extends HookConsumerWidget { - SearchPage({super.key}); - - FocusNode searchFocusNode = FocusNode(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; - final curatedLocation = ref.watch(getCuratedLocationProvider); - final curatedPeople = ref.watch(getCuratedPeopleProvider); - final isMapEnabled = - ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); - double imageSize = math.min(context.width / 3, 150); - - TextStyle categoryTitleStyle = const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 15.0, - ); - - Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; - - useEffect( - () { - searchFocusNode = FocusNode(); - return () => searchFocusNode.dispose(); - }, - [], - ); - - onSearchSubmitted(String searchTerm) async { - searchFocusNode.unfocus(); - ref.watch(searchPageStateProvider.notifier).disableSearch(); - - context.pushRoute( - SearchResultRoute( - searchTerm: searchTerm, - ), - ); - } - - showNameEditModel( - String personId, - String personName, - ) { - return showDialog( - context: context, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ); - } - - buildPeople() { - return SizedBox( - height: imageSize, - child: curatedPeople.widgetWhen( - onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (people) => Padding( - padding: const EdgeInsets.only( - left: 16, - top: 8, - ), - child: CuratedPeopleRow( - content: people.take(12).toList(), - onTap: (content, index) { - context.pushRoute( - PersonResultRoute( - personId: content.id, - personName: content.label, - ), - ); - }, - onNameTap: (person, index) => { - showNameEditModel(person.id, person.label), - }, - ), - ), - ), - ); - } - - buildPlaces() { - return SizedBox( - height: imageSize, - child: curatedLocation.widgetWhen( - onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (locations) => CuratedPlacesRow( - isMapEnabled: isMapEnabled, - content: locations - .map( - (o) => CuratedContent( - id: o.id, - label: o.city, - ), - ) - .toList(), - imageSize: imageSize, - onTap: (content, index) { - context.pushRoute( - SearchResultRoute( - searchTerm: 'm:${content.label}', - ), - ); - }, - ), - ), - ); - } - - return Scaffold( - appBar: ImmichSearchBar( - searchFocusNode: searchFocusNode, - onSubmitted: onSearchSubmitted, - ), - body: GestureDetector( - onTap: () { - searchFocusNode.unfocus(); - ref.watch(searchPageStateProvider.notifier).disableSearch(); - }, - child: Stack( - children: [ - ListView( - children: [ - SearchRowTitle( - title: "search_page_people".tr(), - onViewAllPressed: () => - context.pushRoute(const AllPeopleRoute()), - ), - buildPeople(), - SearchRowTitle( - title: "search_page_places".tr(), - onViewAllPressed: () => - context.pushRoute(const CuratedLocationRoute()), - top: 0, - ), - const SizedBox(height: 10.0), - buildPlaces(), - const SizedBox(height: 24.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'search_page_your_activity', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), - ListTile( - leading: Icon( - Icons.favorite_border_rounded, - color: categoryIconColor, - ), - title: - Text('search_page_favorites', style: categoryTitleStyle) - .tr(), - onTap: () => context.pushRoute(const FavoritesRoute()), - ), - const CategoryDivider(), - ListTile( - leading: Icon( - Icons.schedule_outlined, - color: categoryIconColor, - ), - title: Text( - 'search_page_recently_added', - style: categoryTitleStyle, - ).tr(), - onTap: () => context.pushRoute(const RecentlyAddedRoute()), - ), - const SizedBox(height: 24.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - 'search_page_categories', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ), - ListTile( - title: - Text('search_page_screenshots', style: categoryTitleStyle) - .tr(), - leading: Icon( - Icons.screenshot, - color: categoryIconColor, - ), - onTap: () => context.pushRoute( - SearchResultRoute( - searchTerm: 'screenshots', - ), - ), - ), - const CategoryDivider(), - ListTile( - title: Text('search_page_selfies', style: categoryTitleStyle) - .tr(), - leading: Icon( - Icons.photo_camera_front_outlined, - color: categoryIconColor, - ), - onTap: () => context.pushRoute( - SearchResultRoute( - searchTerm: 'selfies', - ), - ), - ), - const CategoryDivider(), - ListTile( - title: Text('search_page_videos', style: categoryTitleStyle) - .tr(), - leading: Icon( - Icons.play_circle_outline, - color: categoryIconColor, - ), - onTap: () => context.pushRoute(const AllVideosRoute()), - ), - const CategoryDivider(), - ListTile( - title: Text( - 'search_page_motion_photos', - style: categoryTitleStyle, - ).tr(), - leading: Icon( - Icons.motion_photos_on_outlined, - color: categoryIconColor, - ), - onTap: () => context.pushRoute(const AllMotionPhotosRoute()), - ), - ], - ), - if (isSearchEnabled) - SearchSuggestionList(onSubmitted: onSearchSubmitted), - ], - ), - ), - ); - } -} - -class CategoryDivider extends StatelessWidget { - const CategoryDivider({super.key}); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.only( - left: 56, - right: 16, - ), - child: Divider( - height: 0, - ), - ); - } -} +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; +import 'package:immich_mobile/modules/search/providers/people.provider.dart'; +import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; +import 'package:immich_mobile/modules/search/ui/curated_people_row.dart'; +import 'package:immich_mobile/modules/search/ui/curated_places_row.dart'; +import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; +import 'package:immich_mobile/modules/search/ui/search_row_title.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/server_info.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; + +@RoutePage() +// ignore: must_be_immutable +class SearchPage extends HookConsumerWidget { + const SearchPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final curatedLocation = ref.watch(getCuratedLocationProvider); + final curatedPeople = ref.watch(getAllPeopleProvider); + final isMapEnabled = + ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); + double imageSize = math.min(context.width / 3, 150); + + TextStyle categoryTitleStyle = const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15.0, + ); + + Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; + + showNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ); + } + + buildPeople() { + return SizedBox( + height: imageSize, + child: curatedPeople.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (people) => Padding( + padding: const EdgeInsets.only( + left: 16, + top: 8, + ), + child: CuratedPeopleRow( + content: people + .map((e) => CuratedContent(label: e.name, id: e.id)) + .take(12) + .toList(), + onTap: (content, index) { + context.pushRoute( + PersonResultRoute( + personId: content.id, + personName: content.label, + ), + ); + }, + onNameTap: (person, index) => { + showNameEditModel(person.id, person.label), + }, + ), + ), + ), + ); + } + + buildPlaces() { + return SizedBox( + height: imageSize, + child: curatedLocation.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (locations) => CuratedPlacesRow( + isMapEnabled: isMapEnabled, + content: locations + .map( + (o) => CuratedContent( + id: o.id, + label: o.city, + ), + ) + .toList(), + imageSize: imageSize, + onTap: (content, index) { + context.pushRoute( + SearchInputRoute( + prefilter: SearchFilter( + people: {}, + location: SearchLocationFilter( + city: content.label, + ), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: AssetType.other, + ), + ), + ); + }, + ), + ), + ); + } + + buildSearchButton() { + return GestureDetector( + onTap: () { + context.pushRoute(SearchInputRoute()); + }, + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: context.isDarkTheme + ? Colors.grey[800]! + : const Color.fromARGB(255, 225, 225, 225), + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + child: Row( + children: [ + Icon(Icons.search, color: context.primaryColor), + const SizedBox(width: 16.0), + Text( + "Search your photos", + style: context.textTheme.bodyLarge?.copyWith( + color: + context.isDarkTheme ? Colors.white70 : Colors.black54, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ); + } + + return Scaffold( + appBar: const ImmichAppBar(), + body: Stack( + children: [ + ListView( + children: [ + buildSearchButton(), + SearchRowTitle( + title: "search_page_people".tr(), + onViewAllPressed: () => + context.pushRoute(const AllPeopleRoute()), + ), + buildPeople(), + SearchRowTitle( + title: "search_page_places".tr(), + onViewAllPressed: () => + context.pushRoute(const CuratedLocationRoute()), + top: 0, + ), + const SizedBox(height: 10.0), + buildPlaces(), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'search_page_your_activity', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(), + ), + ListTile( + leading: Icon( + Icons.favorite_border_rounded, + color: categoryIconColor, + ), + title: Text('search_page_favorites', style: categoryTitleStyle) + .tr(), + onTap: () => context.pushRoute(const FavoritesRoute()), + ), + const CategoryDivider(), + ListTile( + leading: Icon( + Icons.schedule_outlined, + color: categoryIconColor, + ), + title: Text( + 'search_page_recently_added', + style: categoryTitleStyle, + ).tr(), + onTap: () => context.pushRoute(const RecentlyAddedRoute()), + ), + const SizedBox(height: 24.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'search_page_categories', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(), + ), + ListTile( + title: + Text('search_page_videos', style: categoryTitleStyle).tr(), + leading: Icon( + Icons.play_circle_outline, + color: categoryIconColor, + ), + onTap: () => context.pushRoute(const AllVideosRoute()), + ), + const CategoryDivider(), + ListTile( + title: Text( + 'search_page_motion_photos', + style: categoryTitleStyle, + ).tr(), + leading: Icon( + Icons.motion_photos_on_outlined, + color: categoryIconColor, + ), + onTap: () => context.pushRoute(const AllMotionPhotosRoute()), + ), + ], + ), + ], + ), + ); + } +} + +class CategoryDivider extends StatelessWidget { + const CategoryDivider({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.only( + left: 56, + right: 16, + ), + child: Divider( + height: 0, + ), + ); + } +} diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart deleted file mode 100644 index 97df5f10c..000000000 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ /dev/null @@ -1,213 +0,0 @@ -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/modules/search/providers/search_page_state.provider.dart'; -import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; -import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; -import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; - -class SearchType { - SearchType({required this.isSmart, required this.searchTerm}); - - final bool isSmart; - final String searchTerm; -} - -SearchType _getSearchType(String searchTerm) { - if (searchTerm.startsWith('m:')) { - return SearchType(isSmart: false, searchTerm: searchTerm.substring(2)); - } else { - return SearchType(isSmart: true, searchTerm: searchTerm); - } -} - -@RoutePage() -class SearchResultPage extends HookConsumerWidget { - const SearchResultPage({ - super.key, - required this.searchTerm, - }); - - final String searchTerm; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTermController = useTextEditingController(text: ""); - final isNewSearch = useState(false); - final currentSearchTerm = useState(searchTerm); - - FocusNode? searchFocusNode; - - useEffect( - () { - searchFocusNode = FocusNode(); - - var searchType = _getSearchType(searchTerm); - - Future.delayed( - Duration.zero, - () => ref - .read(searchResultPageProvider.notifier) - .search(searchType.searchTerm, smartSearch: searchType.isSmart), - ); - return () => searchFocusNode?.dispose(); - }, - [], - ); - - Future onSearchSubmitted(String newSearchTerm) { - debugPrint("Re-Search with $newSearchTerm"); - searchFocusNode?.unfocus(); - isNewSearch.value = false; - currentSearchTerm.value = newSearchTerm; - var searchType = _getSearchType(newSearchTerm); - return ref - .watch(searchResultPageProvider.notifier) - .search(searchType.searchTerm, smartSearch: searchType.isSmart); - } - - buildTextField() { - return TextField( - controller: searchTermController, - focusNode: searchFocusNode, - autofocus: false, - onTap: () { - searchTermController.clear(); - ref.watch(searchPageStateProvider.notifier).setSearchTerm(""); - searchFocusNode?.requestFocus(); - }, - textInputAction: TextInputAction.search, - onSubmitted: (searchTerm) { - if (searchTerm.isNotEmpty) { - searchTermController.clear(); - onSearchSubmitted(searchTerm); - } else { - isNewSearch.value = false; - } - }, - onChanged: (value) { - ref.watch(searchPageStateProvider.notifier).setSearchTerm(value); - }, - decoration: InputDecoration( - hintText: 'search_result_page_new_search_hint'.tr(), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - hintStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16.0, - color: context.isDarkTheme - ? Colors.grey[500] - : Colors.black.withOpacity(0.5), - ), - ), - ); - } - - buildChip() { - return Chip( - label: Wrap( - spacing: 5, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - children: [ - Text( - currentSearchTerm.value, - style: TextStyle( - color: context.primaryColor, - fontSize: 13, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - ), - Icon( - Icons.close_rounded, - color: context.primaryColor, - size: 20, - ), - ], - ), - backgroundColor: context.primaryColor.withAlpha(50), - ); - } - - Future refresh() async => onSearchSubmitted(currentSearchTerm.value); - - buildSearchResult() { - final searchResultPageState = ref.watch(searchResultPageProvider); - - if (searchResultPageState.isError) { - return Padding( - padding: const EdgeInsets.all(12), - child: const Text("common_server_error").tr(), - ); - } - - if (searchResultPageState.isLoading) { - return const Center(child: ImmichLoadingIndicator()); - } - - if (searchResultPageState.isSuccess) { - return MultiselectGrid( - renderListProvider: searchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - onRefresh: refresh, - ); - } - - return const SizedBox(); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - splashRadius: 20, - onPressed: () { - if (isNewSearch.value) { - isNewSearch.value = false; - } else { - context.popRoute(true); - } - }, - icon: const Icon(Icons.arrow_back_ios_rounded), - ), - title: GestureDetector( - onTap: () { - isNewSearch.value = true; - searchFocusNode?.requestFocus(); - }, - child: isNewSearch.value ? buildTextField() : buildChip(), - ), - centerTitle: false, - ), - body: GestureDetector( - onTap: () { - if (searchFocusNode != null) { - searchFocusNode?.unfocus(); - } - - ref.watch(searchPageStateProvider.notifier).disableSearch(); - }, - child: Stack( - children: [ - buildSearchResult(), - if (isNewSearch.value) - SearchSuggestionList(onSubmitted: onSearchSubmitted), - ], - ), - ), - ); - } -} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index f5c1a95d9..46cd7522d 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -31,7 +31,9 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart'; import 'package:immich_mobile/modules/login/views/login_page.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart'; +import 'package:immich_mobile/modules/search/models/search_filter.dart'; import 'package:immich_mobile/modules/settings/views/settings_sub_page.dart'; +import 'package:immich_mobile/modules/search/views/search_input_page.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart'; import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart'; @@ -43,7 +45,6 @@ import 'package:immich_mobile/modules/search/views/curated_location_page.dart'; import 'package:immich_mobile/modules/search/views/person_result_page.dart'; import 'package:immich_mobile/modules/search/views/recently_added_page.dart'; import 'package:immich_mobile/modules/search/views/search_page.dart'; -import 'package:immich_mobile/modules/search/views/search_result_page.dart'; import 'package:immich_mobile/modules/settings/views/settings_page.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/custom_transition_builders.dart'; @@ -125,10 +126,6 @@ class AppRouter extends _$AppRouter { page: BackupControllerRoute.page, guards: [_authGuard, _duplicateGuard, _backupPermissionGuard], ), - AutoRoute( - page: SearchResultRoute.page, - guards: [_authGuard, _duplicateGuard], - ), AutoRoute( page: CuratedLocationRoute.page, guards: [_authGuard, _duplicateGuard], @@ -223,6 +220,11 @@ class AppRouter extends _$AppRouter { page: BackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard], ), + CustomRoute( + page: SearchInputRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.noTransition, + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index cc86b701a..fa9fa32f6 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -255,22 +255,21 @@ abstract class _$AppRouter extends RootStackRouter { child: const RecentlyAddedPage(), ); }, - SearchRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SearchRouteArgs()); + SearchInputRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const SearchInputRouteArgs()); return AutoRoutePage( routeData: routeData, - child: SearchPage(key: args.key), + child: SearchInputPage( + key: args.key, + prefilter: args.prefilter, + ), ); }, - SearchResultRoute.name: (routeData) { - final args = routeData.argsAs(); + SearchRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: SearchResultPage( - key: args.key, - searchTerm: args.searchTerm, - ), + child: const SearchPage(), ); }, SelectAdditionalUserForSharingRoute.name: (routeData) { @@ -1113,69 +1112,55 @@ class RecentlyAddedRoute extends PageRouteInfo { } /// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ +/// [SearchInputPage] +class SearchInputRoute extends PageRouteInfo { + SearchInputRoute({ Key? key, + SearchFilter? prefilter, List? children, }) : super( + SearchInputRoute.name, + args: SearchInputRouteArgs( + key: key, + prefilter: prefilter, + ), + initialChildren: children, + ); + + static const String name = 'SearchInputRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class SearchInputRouteArgs { + const SearchInputRouteArgs({ + this.key, + this.prefilter, + }); + + final Key? key; + + final SearchFilter? prefilter; + + @override + String toString() { + return 'SearchInputRouteArgs{key: $key, prefilter: $prefilter}'; + } +} + +/// generated route for +/// [SearchPage] +class SearchRoute extends PageRouteInfo { + const SearchRoute({List? children}) + : super( SearchRoute.name, - args: SearchRouteArgs(key: key), initialChildren: children, ); static const String name = 'SearchRoute'; - static const PageInfo page = PageInfo(name); -} - -class SearchRouteArgs { - const SearchRouteArgs({this.key}); - - final Key? key; - - @override - String toString() { - return 'SearchRouteArgs{key: $key}'; - } -} - -/// generated route for -/// [SearchResultPage] -class SearchResultRoute extends PageRouteInfo { - SearchResultRoute({ - Key? key, - required String searchTerm, - List? children, - }) : super( - SearchResultRoute.name, - args: SearchResultRouteArgs( - key: key, - searchTerm: searchTerm, - ), - initialChildren: children, - ); - - static const String name = 'SearchResultRoute'; - - static const PageInfo page = - PageInfo(name); -} - -class SearchResultRouteArgs { - const SearchResultRouteArgs({ - this.key, - required this.searchTerm, - }); - - final Key? key; - - final String searchTerm; - - @override - String toString() { - return 'SearchResultRouteArgs{key: $key, searchTerm: $searchTerm}'; - } + static const PageInfo page = PageInfo(name); } /// generated route for diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index dafbedd31..afe87fb24 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -38,7 +38,7 @@ class TabNavigationObserver extends AutoRouterObserver { if (route.name == 'SearchRoute') { // Refresh Location State ref.invalidate(getCuratedLocationProvider); - ref.invalidate(getCuratedPeopleProvider); + ref.invalidate(getAllPeopleProvider); } if (route.name == 'SharingRoute') { diff --git a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart index 495f8f2f9..c9cc6c04a 100644 --- a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart +++ b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart @@ -43,6 +43,7 @@ class MultiselectGrid extends HookConsumerWidget { this.editEnabled = false, this.unarchive = false, this.unfavorite = false, + this.emptyIndicator, }); final ProviderListenable> renderListProvider; @@ -57,12 +58,12 @@ class MultiselectGrid extends HookConsumerWidget { final bool favoriteEnabled; final bool unfavorite; final bool editEnabled; - + final Widget? emptyIndicator; Widget buildDefaultLoadingIndicator() => const Center(child: ImmichLoadingIndicator()); Widget buildEmptyIndicator() => - const Center(child: Text("No assets to show")); + emptyIndicator ?? const Center(child: Text("No assets to show")); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 40850bdb4..40de493d0 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; -import 'package:immich_mobile/modules/search/ui/immich_search_bar.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/tab.provider.dart'; @@ -53,10 +52,6 @@ class TabControllerPage extends HookConsumerWidget { // Scroll to top scrollToTopNotifierProvider.scrollToTop(); } - if (tabsRouter.activeIndex == 1 && index == 1) { - // Focus search - searchFocusNotifier.requestFocus(); - } HapticFeedback.selectionClick(); tabsRouter.setActiveIndex(index); @@ -111,10 +106,7 @@ class TabControllerPage extends HookConsumerWidget { // Scroll to top scrollToTopNotifierProvider.scrollToTop(); } - if (tabsRouter.activeIndex == 1 && index == 1) { - // Focus search - searchFocusNotifier.requestFocus(); - } + HapticFeedback.selectionClick(); tabsRouter.setActiveIndex(index); ref.read(tabProvider.notifier).state = TabEnum.values[index]; @@ -170,11 +162,11 @@ class TabControllerPage extends HookConsumerWidget { final multiselectEnabled = ref.watch(multiselectProvider); return AutoTabsRouter( - routes: [ - const HomeRoute(), + routes: const [ + HomeRoute(), SearchRoute(), - const SharingRoute(), - const LibraryRoute(), + SharingRoute(), + LibraryRoute(), ], duration: const Duration(milliseconds: 600), transitionBuilder: (context, child, animation) => FadeTransition( diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index e2ed6cd56..07fac00e4 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -33,6 +33,9 @@ final ThemeData base = ThemeData( final ThemeData immichLightTheme = ThemeData( useMaterial3: true, brightness: Brightness.light, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.indigo, + ), primarySwatch: Colors.indigo, primaryColor: Colors.indigo, hintColor: Colors.indigo, @@ -158,6 +161,10 @@ final ThemeData immichDarkTheme = ThemeData( brightness: Brightness.dark, primarySwatch: Colors.indigo, primaryColor: immichDarkThemePrimaryColor, + colorScheme: ColorScheme.fromSeed( + seedColor: immichDarkThemePrimaryColor, + brightness: Brightness.dark, + ), scaffoldBackgroundColor: immichDarkBackgroundColor, hintColor: Colors.grey[600], fontFamily: 'Overpass', diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index bf01dfcc8..6bf2b0902 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1,1816 +1,1808 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" - url: "https://pub.dev" - source: hosted - version: "67.0.0" - analyzer: - dependency: "direct overridden" - description: - name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - analyzer_plugin: - dependency: "direct overridden" - description: - name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" - url: "https://pub.dev" - source: hosted - version: "0.11.3" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - archive: - dependency: transitive - description: - name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" - url: "https://pub.dev" - source: hosted - version: "3.4.10" - args: - dependency: transitive - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: "direct main" - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - auto_route: - dependency: "direct main" - description: - name: auto_route - sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1" - url: "https://pub.dev" - source: hosted - version: "7.8.4" - auto_route_generator: - dependency: "direct dev" - description: - name: auto_route_generator - sha256: "11067a3bcd643812518fe26c0c9ec073990286cabfd9d74b6da9ef9b913c4d22" - url: "https://pub.dev" - source: hosted - version: "7.3.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" - url: "https://pub.dev" - source: hosted - version: "4.0.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" - url: "https://pub.dev" - source: hosted - version: "2.4.8" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" - url: "https://pub.dev" - source: hosted - version: "7.3.0" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 - url: "https://pub.dev" - source: hosted - version: "8.9.0" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - cancellation_token: - dependency: transitive - description: - name: cancellation_token - sha256: ad95acf9d4b2f3563e25dc937f63587e46a70ce534e910b65d10e115490f1027 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - cancellation_token_http: - dependency: "direct main" - description: - name: cancellation_token_http - sha256: "37ad2a20dba02aeb1f0a4d845e7a57eebacdb709e1186e0491e7cd81c559c4ff" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" - url: "https://pub.dev" - source: hosted - version: "1.7.5" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 - url: "https://pub.dev" - source: hosted - version: "0.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 - url: "https://pub.dev" - source: hosted - version: "4.10.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - connectivity_plus: - dependency: "direct main" - description: - name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a - url: "https://pub.dev" - source: hosted - version: "1.2.4" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e - url: "https://pub.dev" - source: hosted - version: "0.3.3+8" - crypto: - dependency: transitive - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - csslib: - dependency: transitive - description: - name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - cupertino_icons: - dependency: transitive - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.dev" - source: hosted - version: "1.0.6" - custom_lint: - dependency: "direct dev" - description: - name: custom_lint - sha256: f89ff83efdba7c8996e86bb3bad0b759d58f9b19ae4d0e277a386ddd8b481217 - url: "https://pub.dev" - source: hosted - version: "0.6.0" - custom_lint_builder: - dependency: transitive - description: - name: custom_lint_builder - sha256: "3a14687fc71a5e2124a29722106f7b7e67dd5a6d58e33f2859650b46acff1d54" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "1e9128e095ad5e0973469bdaac1ead8bfc86c485954c23cf617299de5e6fa029" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" - url: "https://pub.dev" - source: hosted - version: "2.3.4" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - device_info_plus: - dependency: "direct main" - description: - name: device_info_plus - sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" - url: "https://pub.dev" - source: hosted - version: "9.1.2" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - easy_image_viewer: - dependency: "direct main" - description: - name: easy_image_viewer - sha256: "750bb85e0a34504557d378a616110540caeec2324490fc040709589219e75834" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - easy_localization: - dependency: "direct main" - description: - name: easy_localization - sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af" - url: "https://pub.dev" - source: hosted - version: "3.0.4" - easy_logger: - dependency: transitive - description: - name: easy_logger - sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 - url: "https://pub.dev" - source: hosted - version: "0.0.2" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - file: - dependency: transitive - description: - name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" - url: "https://pub.dev" - source: hosted - version: "0.9.2+1" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 - url: "https://pub.dev" - source: hosted - version: "0.9.3+3" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" - source: hosted - version: "2.6.2" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 - url: "https://pub.dev" - source: hosted - version: "0.9.3+1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - flutter_displaymode: - dependency: "direct main" - description: - name: flutter_displaymode - sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef" - url: "https://pub.dev" - source: hosted - version: "0.6.0" - flutter_driver: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_hooks: - dependency: "direct main" - description: - name: flutter_hooks - sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 - url: "https://pub.dev" - source: hosted - version: "0.20.5" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" - url: "https://pub.dev" - source: hosted - version: "0.13.1" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 - url: "https://pub.dev" - source: hosted - version: "16.3.2" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" - url: "https://pub.dev" - source: hosted - version: "4.0.0+1" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" - url: "https://pub.dev" - source: hosted - version: "7.0.0+1" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_native_splash: - dependency: "direct dev" - description: - name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" - url: "https://pub.dev" - source: hosted - version: "2.3.10" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da - url: "https://pub.dev" - source: hosted - version: "2.0.17" - flutter_riverpod: - dependency: transitive - description: - name: flutter_riverpod - sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c - url: "https://pub.dev" - source: hosted - version: "2.0.9" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_udid: - dependency: "direct main" - description: - name: flutter_udid - sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - flutter_web_auth: - dependency: "direct main" - description: - name: flutter_web_auth - sha256: a69fa8f43b9e4d86ac72176bf747b735e7b977dd7cf215076d95b87cb05affdd - url: "https://pub.dev" - source: hosted - version: "0.5.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - fluttertoast: - dependency: "direct main" - description: - name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 - url: "https://pub.dev" - source: hosted - version: "8.2.4" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d - url: "https://pub.dev" - source: hosted - version: "2.4.1" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - geolocator: - dependency: "direct main" - description: - name: geolocator - sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" - url: "https://pub.dev" - source: hosted - version: "11.0.0" - geolocator_android: - dependency: transitive - description: - name: geolocator_android - sha256: "136f1c97e1903366393bda514c5d9e98843418baea52899aa45edae9af8a5cd6" - url: "https://pub.dev" - source: hosted - version: "4.5.2" - geolocator_apple: - dependency: transitive - description: - name: geolocator_apple - sha256: "2f2d4ee16c4df269e93c0e382be075cc01d5db6703c3196e4af20a634fe49ef4" - url: "https://pub.dev" - source: hosted - version: "2.3.6" - geolocator_platform_interface: - dependency: transitive - description: - name: geolocator_platform_interface - sha256: "009a21c4bc2761e58dccf07c24f219adaebe0ff707abdfd40b0a763d4003fab9" - url: "https://pub.dev" - source: hosted - version: "4.2.2" - geolocator_web: - dependency: transitive - description: - name: geolocator_web - sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - geolocator_windows: - dependency: transitive - description: - name: geolocator_windows - sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af - url: "https://pub.dev" - source: hosted - version: "0.2.2" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - graphs: - dependency: transitive - description: - name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 - url: "https://pub.dev" - source: hosted - version: "2.3.1" - hooks_riverpod: - dependency: "direct main" - description: - name: hooks_riverpod - sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - hotreloader: - dependency: transitive - description: - name: hotreloader - sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e - url: "https://pub.dev" - source: hosted - version: "4.2.0" - html: - dependency: transitive - description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" - http: - dependency: "direct main" - description: - name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" - source: hosted - version: "0.13.6" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: "direct main" - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - image: - dependency: transitive - description: - name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" - url: "https://pub.dev" - source: hosted - version: "4.1.7" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" - url: "https://pub.dev" - source: hosted - version: "1.0.7" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" - url: "https://pub.dev" - source: hosted - version: "0.8.9+3" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 - url: "https://pub.dev" - source: hosted - version: "3.0.2" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 - url: "https://pub.dev" - source: hosted - version: "0.8.9+1" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b - url: "https://pub.dev" - source: hosted - version: "2.9.3" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - integration_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" - url: "https://pub.dev" - source: hosted - version: "0.18.1" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - isar: - dependency: "direct main" - description: - name: isar - sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_flutter_libs: - dependency: "direct main" - description: - name: isar_flutter_libs - sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_generator: - dependency: "direct dev" - description: - name: isar_generator - sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - logging: - dependency: "direct main" - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - maplibre_gl: - dependency: "direct main" - description: - path: "." - ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" - maplibre_gl_platform_interface: - dependency: transitive - description: - path: maplibre_gl_platform_interface - ref: main - resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1" - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" - maplibre_gl_web: - dependency: transitive - description: - path: maplibre_gl_web - ref: main - resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1" - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" - source: hosted - version: "0.12.16+1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" - url: "https://pub.dev" - source: hosted - version: "0.8.0" - meta: - dependency: "direct overridden" - description: - name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" - url: "https://pub.dev" - source: hosted - version: "1.12.0" - mime: - dependency: transitive - description: - name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" - url: "https://pub.dev" - source: hosted - version: "1.0.5" - mocktail: - dependency: "direct dev" - description: - name: mocktail - sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - nm: - dependency: transitive - description: - name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - octo_image: - dependency: "direct main" - description: - name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - openapi: - dependency: "direct main" - description: - path: openapi - relative: true - source: path - version: "1.0.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" - url: "https://pub.dev" - source: hosted - version: "5.0.1" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - path: - dependency: "direct main" - description: - name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" - source: hosted - version: "1.9.0" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf - url: "https://pub.dev" - source: hosted - version: "1.0.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - path_provider_ios: - dependency: "direct main" - description: - name: path_provider_ios - sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" - url: "https://pub.dev" - source: hosted - version: "2.0.11" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" - url: "https://pub.dev" - source: hosted - version: "11.3.0" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" - url: "https://pub.dev" - source: hosted - version: "12.0.5" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b - url: "https://pub.dev" - source: hosted - version: "9.4.0" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" - url: "https://pub.dev" - source: hosted - version: "0.1.1" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" - url: "https://pub.dev" - source: hosted - version: "4.2.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - photo_manager: - dependency: "direct main" - description: - name: photo_manager - sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61" - url: "https://pub.dev" - source: hosted - version: "3.0.0-dev.5" - photo_manager_image_provider: - dependency: "direct main" - description: - name: photo_manager_image_provider - sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 - url: "https://pub.dev" - source: hosted - version: "2.1.0" - platform: - dependency: transitive - description: - name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - provider: - dependency: transitive - description: - name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" - url: "https://pub.dev" - source: hosted - version: "6.1.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - riverpod: - dependency: transitive - description: - name: riverpod - sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - riverpod_analyzer_utils: - dependency: transitive - description: - name: riverpod_analyzer_utils - sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - riverpod_annotation: - dependency: "direct main" - description: - name: riverpod_annotation - sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95" - url: "https://pub.dev" - source: hosted - version: "2.3.4" - riverpod_generator: - dependency: "direct dev" - description: - name: riverpod_generator - sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32" - url: "https://pub.dev" - source: hosted - version: "2.3.11" - riverpod_lint: - dependency: "direct dev" - description: - name: riverpod_lint - sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937 - url: "https://pub.dev" - source: hosted - version: "2.3.9" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" - scrollable_positioned_list: - dependency: "direct main" - description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - url: "https://pub.dev" - source: hosted - version: "0.3.8" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" - url: "https://pub.dev" - source: hosted - version: "7.2.2" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 - url: "https://pub.dev" - source: hosted - version: "3.3.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" - url: "https://pub.dev" - source: hosted - version: "2.3.5" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - socket_io_client: - dependency: "direct main" - description: - name: socket_io_client - sha256: ede469f3e4c55e8528b4e023bdedbc20832e8811ab9b61679d1ba3ed5f01f23b - url: "https://pub.dev" - source: hosted - version: "2.0.3+1" - socket_io_common: - dependency: transitive - description: - name: socket_io_common - sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb" - url: "https://pub.dev" - source: hosted - version: "2.0.3" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 - url: "https://pub.dev" - source: hosted - version: "2.3.2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - state_notifier: - dependency: transitive - description: - name: state_notifier - sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" - source: hosted - version: "1.0.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - sync_http: - dependency: transitive - description: - name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - thumbhash: - dependency: "direct main" - description: - name: thumbhash - sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37" - url: "https://pub.dev" - source: hosted - version: "0.1.0+1" - time: - dependency: transitive - description: - name: time - sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 - url: "https://pub.dev" - source: hosted - version: "2.1.4" - timezone: - dependency: "direct main" - description: - name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" - url: "https://pub.dev" - source: hosted - version: "0.9.2" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c - url: "https://pub.dev" - source: hosted - version: "6.2.4" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 - url: "https://pub.dev" - source: hosted - version: "6.3.0" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" - url: "https://pub.dev" - source: hosted - version: "6.2.4" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 - url: "https://pub.dev" - source: hosted - version: "3.1.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b - url: "https://pub.dev" - source: hosted - version: "2.2.3" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 - url: "https://pub.dev" - source: hosted - version: "3.1.1" - uuid: - dependency: transitive - description: - name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 - url: "https://pub.dev" - source: hosted - version: "4.3.3" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" - url: "https://pub.dev" - source: hosted - version: "1.1.10+1" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 - url: "https://pub.dev" - source: hosted - version: "1.1.10+1" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" - url: "https://pub.dev" - source: hosted - version: "1.1.10+1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 - url: "https://pub.dev" - source: hosted - version: "2.8.2" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" - url: "https://pub.dev" - source: hosted - version: "2.4.12" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" - url: "https://pub.dev" - source: hosted - version: "6.2.2" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" - url: "https://pub.dev" - source: hosted - version: "2.1.3" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d - url: "https://pub.dev" - source: hosted - version: "1.1.4" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webdriver: - dependency: transitive - description: - name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" - url: "https://pub.dev" - source: hosted - version: "3.0.3" - win32: - dependency: transitive - description: - name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" - url: "https://pub.dev" - source: hosted - version: "5.2.0" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d - url: "https://pub.dev" - source: hosted - version: "1.0.4" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 - url: "https://pub.dev" - source: hosted - version: "1.0.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" + url: "https://pub.dev" + source: hosted + version: "66.0.0" + analyzer: + dependency: "direct overridden" + description: + name: analyzer + sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + analyzer_plugin: + dependency: "direct overridden" + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: "direct main" + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1" + url: "https://pub.dev" + source: hosted + version: "7.8.4" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: "11067a3bcd643812518fe26c0c9ec073990286cabfd9d74b6da9ef9b913c4d22" + url: "https://pub.dev" + source: hosted + version: "7.3.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + cancellation_token: + dependency: transitive + description: + name: cancellation_token + sha256: ad95acf9d4b2f3563e25dc937f63587e46a70ce534e910b65d10e115490f1027 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + cancellation_token_http: + dependency: "direct main" + description: + name: cancellation_token_http + sha256: "37ad2a20dba02aeb1f0a4d845e7a57eebacdb709e1186e0491e7cd81c559c4ff" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" + url: "https://pub.dev" + source: hosted + version: "1.7.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 + url: "https://pub.dev" + source: hosted + version: "0.6.4" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + url: "https://pub.dev" + source: hosted + version: "0.6.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + url: "https://pub.dev" + source: hosted + version: "9.1.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + easy_image_viewer: + dependency: "direct main" + description: + name: easy_image_viewer + sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_displaymode: + dependency: "direct main" + description: + name: flutter_displaymode + sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" + url: "https://pub.dev" + source: hosted + version: "0.20.4" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 + url: "https://pub.dev" + source: hosted + version: "16.3.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + url: "https://pub.dev" + source: hosted + version: "2.3.10" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_udid: + dependency: "direct main" + description: + name: flutter_udid + sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_web_auth: + dependency: "direct main" + description: + name: flutter_web_auth + sha256: a69fa8f43b9e4d86ac72176bf747b735e7b977dd7cf215076d95b87cb05affdd + url: "https://pub.dev" + source: hosted + version: "0.5.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + url: "https://pub.dev" + source: hosted + version: "8.2.4" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" + url: "https://pub.dev" + source: hosted + version: "11.0.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" + url: "https://pub.dev" + source: hosted + version: "4.3.1" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af + url: "https://pub.dev" + source: hosted + version: "0.2.2" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: "direct main" + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + url: "https://pub.dev" + source: hosted + version: "0.8.7+4" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + url: "https://pub.dev" + source: hosted + version: "0.8.8" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + url: "https://pub.dev" + source: hosted + version: "2.9.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + isar: + dependency: "direct main" + description: + name: isar + sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + isar_flutter_libs: + dependency: "direct main" + description: + name: isar_flutter_libs + sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + isar_generator: + dependency: "direct dev" + description: + name: isar_generator + sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + maplibre_gl: + dependency: "direct main" + description: + path: "." + ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + maplibre_gl_platform_interface: + dependency: transitive + description: + path: maplibre_gl_platform_interface + ref: main + resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + maplibre_gl_web: + dependency: transitive + description: + path: maplibre_gl_web + ref: main + resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: "direct overridden" + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: "direct main" + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + openapi: + dependency: "direct main" + description: + path: openapi + relative: true + source: path + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path: + dependency: "direct main" + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + path_provider_ios: + dependency: "direct main" + description: + name: path_provider_ios + sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" + url: "https://pub.dev" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + url: "https://pub.dev" + source: hosted + version: "2.2.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" + url: "https://pub.dev" + source: hosted + version: "11.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 + url: "https://pub.dev" + source: hosted + version: "9.3.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + photo_manager: + dependency: "direct main" + description: + name: photo_manager + sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.5" + photo_manager_image_provider: + dependency: "direct main" + description: + name: photo_manager_image_provider + sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + provider: + dependency: transitive + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + url: "https://pub.dev" + source: hosted + version: "2.5.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 + url: "https://pub.dev" + source: hosted + version: "2.3.9" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + url: "https://pub.dev" + source: hosted + version: "2.3.10" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.dev" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + url: "https://pub.dev" + source: hosted + version: "3.3.1" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socket_io_client: + dependency: "direct main" + description: + name: socket_io_client + sha256: ede469f3e4c55e8528b4e023bdedbc20832e8811ab9b61679d1ba3ed5f01f23b + url: "https://pub.dev" + source: hosted + version: "2.0.3+1" + socket_io_common: + dependency: transitive + description: + name: socket_io_common + sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + thumbhash: + dependency: "direct main" + description: + name: thumbhash + sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37" + url: "https://pub.dev" + source: hosted + version: "0.1.0+1" + time: + dependency: transitive + description: + name: time + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + url: "https://pub.dev" + source: hosted + version: "6.2.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + url: "https://pub.dev" + source: hosted + version: "2.8.2" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + url: "https://pub.dev" + source: hosted + version: "2.4.9" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + url: "https://pub.dev" + source: hosted + version: "1.1.4" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" From ee4d9fff161ce6e84db40348f4749a046a7ed8cf Mon Sep 17 00:00:00 2001 From: ZlabiDev Date: Mon, 1 Apr 2024 18:06:25 +0200 Subject: [PATCH 16/16] fixes issue #8352 (#8432) fixed issue #8352 --- server/src/services/asset.service.spec.ts | 4 +++- server/src/services/asset.service.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) mode change 100644 => 100755 server/src/services/asset.service.spec.ts diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts old mode 100644 new mode 100755 index f7e502e69..683e2f5ae --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -311,13 +311,15 @@ describe(AssetService.name, () => { const image1 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 0, 0, 0) }; const image2 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 1, 0, 0) }; const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) }; + const image4 = { ...assetStub.image, localDateTime: new Date(2009, 1, 15) }; partnerMock.getAll.mockResolvedValue([]); - assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3]); + assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3, image4]); await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([ { yearsAgo: 1, title: '1 year since...', assets: [mapAsset(image1), mapAsset(image2)] }, { yearsAgo: 9, title: '9 years since...', assets: [mapAsset(image3)] }, + { yearsAgo: 15, title: '15 years since...', assets: [mapAsset(image4)] }, ]); expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index b8a9dec87..556883afd 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -184,7 +184,7 @@ export class AssetService { return Object.keys(groups) .map(Number) - .sort() + .sort((a, b) => a - b) .filter((yearsAgo) => yearsAgo > 0) .map((yearsAgo) => ({ yearsAgo,