mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 532567ab2a | |||
| dd97395f3a | |||
| 7ae268e287 | |||
| f07e2b58f0 | |||
| 4b8f90aa55 | |||
| 55ee9f76da | |||
| 30f6d4439e | |||
| f62d98a0d1 | |||
| db3d580761 | |||
| 0bc38fefe6 | |||
| acc4219849 | |||
| 5234e21241 | |||
| 17b327bfcd | |||
| d14d0a9b9b | |||
| bf47147fbb | |||
| 9ea0a69a72 | |||
| 00f43ffc25 | |||
| 96dc4a77a0 | |||
| db7158b967 | |||
| e5722c525b | |||
| f616de5af8 | |||
| 4f39663d27 | |||
| 367025a3a8 | |||
| 60dafecdc9 | |||
| 16c1c3c780 | |||
| e633bc3f24 | |||
| a07d7b0c82 | |||
| a469d350be | |||
| ccab4c88bb | |||
| 430638e129 | |||
| caebe5166a | |||
| 1bd28c3e78 | |||
| 31a55aaa73 | |||
| 8b2e1509ff | |||
| d0cb97f994 | |||
| f0cf3311d5 | |||
| 3ce0654cab | |||
| f0e2fced57 | |||
| 8ba20cbd44 | |||
| 1d25267f22 | |||
| a4d95b7aba | |||
| 25d0bdc9f5 | |||
| 905b9bd560 | |||
| 672743f543 | |||
| 27c45b5ddb |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install chromium --only-shell
|
run: pnpm exec playwright install chromium --only-shell
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ attach-server:
|
|||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
|
|
||||||
renovate:
|
renovate:
|
||||||
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
|
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
|
||||||
|
|
||||||
# Directories that need to be created for volumes or build output
|
# Directories that need to be created for volumes or build output
|
||||||
VOLUME_DIRS = \
|
VOLUME_DIRS = \
|
||||||
|
|||||||
+6
-6
@@ -13,7 +13,7 @@
|
|||||||
"cli"
|
"cli"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^10.0.0",
|
||||||
"@immich/sdk": "workspace:*",
|
"@immich/sdk": "workspace:*",
|
||||||
"@types/byte-size": "^8.1.0",
|
"@types/byte-size": "^8.1.0",
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^63.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^17.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:dev": "vite build --sourcemap true",
|
"build:dev": "vite build --sourcemap true",
|
||||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "pnpm run lint --fix",
|
||||||
"prepack": "npm run build",
|
"prepack": "pnpm run build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:cov": "vitest --coverage",
|
"test:cov": "vitest --coverage",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ There is an automatic scan job that is scheduled to run once a day. Its schedule
|
|||||||
|
|
||||||
This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page.
|
This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page.
|
||||||
|
|
||||||
|
### Deleting a Library
|
||||||
|
|
||||||
|
When deleting an external library, all assets inside are immediately deleted along with the library. Note that while a library can take a long time to fully delete in the background, it is immediately removed from the library list. If the deletion process is interrupted (for example, due to server restart), it will be cleaned up in the next nightly cron job. The cleanup process can also be manually initiated by clicking the "Scan All Libraries" button in the library list.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:
|
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"start": "docusaurus start --port 3005",
|
"start": "docusaurus start --port 3005",
|
||||||
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
"copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
||||||
"build": "npm run copy:openapi && docusaurus build",
|
"build": "pnpm run copy:openapi && docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"clear": "docusaurus clear",
|
"clear": "docusaurus clear",
|
||||||
|
|||||||
+12
-12
@@ -8,23 +8,23 @@
|
|||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:maintenance": "vitest --run --config vitest.maintenance.config.ts",
|
"test:maintenance": "vitest --run --config vitest.maintenance.config.ts",
|
||||||
"test:web": "npx playwright test --project=web",
|
"test:web": "pnpm exec playwright test --project=web",
|
||||||
"test:web:maintenance": "npx playwright test --project=maintenance",
|
"test:web:maintenance": "pnpm exec playwright test --project=maintenance",
|
||||||
"test:web:ui": "npx playwright test --project=ui",
|
"test:web:ui": "pnpm exec playwright test --project=ui",
|
||||||
"start:web": "npx playwright test --ui --project=web",
|
"start:web": "pnpm exec playwright test --ui --project=web",
|
||||||
"start:web:maintenance": "npx playwright test --ui --project=maintenance",
|
"start:web:maintenance": "pnpm exec playwright test --ui --project=maintenance",
|
||||||
"start:web:ui": "npx playwright test --ui --project=ui",
|
"start:web:ui": "pnpm exec playwright test --ui --project=ui",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "pnpm run lint --fix",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^10.0.0",
|
||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@immich/cli": "workspace:*",
|
"@immich/cli": "workspace:*",
|
||||||
"@immich/e2e-auth-server": "workspace:*",
|
"@immich/e2e-auth-server": "workspace:*",
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^63.0.0",
|
||||||
"exiftool-vendored": "^34.3.0",
|
"exiftool-vendored": "^35.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^17.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ test.describe('Shared Links', () => {
|
|||||||
await page.goto(`/share/${sharedLink.key}`);
|
await page.goto(`/share/${sharedLink.key}`);
|
||||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||||
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
||||||
await page.waitForSelector('[data-group] svg');
|
await page.waitForSelector(`[data-asset-id="${asset.id}"] [role="checkbox"]`);
|
||||||
await page.getByRole('checkbox').click();
|
|
||||||
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
|
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ test.describe('Timeline', () => {
|
|||||||
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
||||||
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
||||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||||
await thumbnailUtils.expectSelectedReadonly(page, asset.id);
|
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||||
});
|
});
|
||||||
test('Add photos to album', async ({ page }) => {
|
test('Add photos to album', async ({ page }) => {
|
||||||
const album = timelineRestData.album;
|
const album = timelineRestData.album;
|
||||||
@@ -447,7 +447,7 @@ test.describe('Timeline', () => {
|
|||||||
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
const asset = getAsset(timelineRestData, album.assetIds[0])!;
|
||||||
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
await pageUtils.goToAsset(page, asset.fileCreatedAt);
|
||||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||||
await thumbnailUtils.expectSelectedReadonly(page, asset.id);
|
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||||
await pageUtils.selectDay(page, 'Tue, Feb 27, 2024');
|
await pageUtils.selectDay(page, 'Tue, Feb 27, 2024');
|
||||||
const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => {
|
const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => {
|
||||||
const requestJson = request.postDataJSON();
|
const requestJson = request.postDataJSON();
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export const thumbnailUtils = {
|
|||||||
async expectThumbnailIsNotArchive(page: Page, assetId: string) {
|
async expectThumbnailIsNotArchive(page: Page, assetId: string) {
|
||||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
||||||
},
|
},
|
||||||
async expectSelectedReadonly(page: Page, assetId: string) {
|
async expectSelectedDisabled(page: Page, assetId: string) {
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected]`),
|
page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected][data-disabled]`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
},
|
},
|
||||||
async expectTimelineHasOnScreenAssets(page: Page) {
|
async expectTimelineHasOnScreenAssets(page: Page) {
|
||||||
|
|||||||
@@ -2026,6 +2026,9 @@
|
|||||||
"set_profile_picture": "Set profile picture",
|
"set_profile_picture": "Set profile picture",
|
||||||
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
||||||
"set_stack_primary_asset": "Set as primary asset",
|
"set_stack_primary_asset": "Set as primary asset",
|
||||||
|
"setting_image_navigation_enable_subtitle": "If enabled, you can navigate to the previous/next image by tapping the leftmost/rightmost quarter of the screen.",
|
||||||
|
"setting_image_navigation_enable_title": "Tap to Navigate",
|
||||||
|
"setting_image_navigation_title": "Image Navigation",
|
||||||
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
|
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
|
||||||
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
|
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
|
||||||
"setting_image_viewer_original_title": "Load original image",
|
"setting_image_viewer_original_title": "Load original image",
|
||||||
|
|||||||
Generated
+3
-17
@@ -654,18 +654,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ftfy"
|
|
||||||
version = "6.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "wcwidth" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gevent"
|
name = "gevent"
|
||||||
version = "24.10.3"
|
version = "24.10.3"
|
||||||
@@ -788,14 +776,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "23.0.0"
|
version = "25.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -939,7 +927,6 @@ source = { editable = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiocache" },
|
{ name = "aiocache" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "ftfy" },
|
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "huggingface-hub" },
|
{ name = "huggingface-hub" },
|
||||||
{ name = "insightface" },
|
{ name = "insightface" },
|
||||||
@@ -1018,7 +1005,6 @@ types = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
|
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
||||||
{ name = "ftfy", specifier = ">=6.1.1" },
|
|
||||||
{ name = "gunicorn", specifier = ">=21.1.0" },
|
{ name = "gunicorn", specifier = ">=21.1.0" },
|
||||||
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
|
{ name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
|
||||||
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.13.1"
|
node = "24.13.1"
|
||||||
flutter = "3.41.2"
|
flutter = "3.35.7"
|
||||||
pnpm = "10.29.3"
|
pnpm = "10.30.0"
|
||||||
terragrunt = "0.98.0"
|
terragrunt = "0.98.0"
|
||||||
opentofu = "1.11.4"
|
opentofu = "1.11.4"
|
||||||
java = "21.0.2"
|
java = "21.0.2"
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
|||||||
try {
|
try {
|
||||||
val buffer = NativeBuffer.wrap(pointer, size)
|
val buffer = NativeBuffer.wrap(pointer, size)
|
||||||
copyPixelsToBuffer(buffer)
|
copyPixelsToBuffer(buffer)
|
||||||
recycle()
|
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"pointer" to pointer,
|
"pointer" to pointer,
|
||||||
"width" to width.toLong(),
|
"width" to width.toLong(),
|
||||||
@@ -57,8 +56,9 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
|||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
NativeBuffer.free(pointer)
|
NativeBuffer.free(pointer)
|
||||||
recycle()
|
|
||||||
throw e
|
throw e
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
+16
-22
@@ -38,10 +38,10 @@ PODS:
|
|||||||
- local_auth_darwin (0.0.1):
|
- local_auth_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- MapLibre (6.23.0)
|
- MapLibre (6.14.0)
|
||||||
- maplibre_ios (0.0.1):
|
- maplibre_gl (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- MapLibre (~> 6.21)
|
- MapLibre (= 6.14.0)
|
||||||
- native_video_player (1.0.0):
|
- native_video_player (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- network_info_plus (0.0.1):
|
- network_info_plus (0.0.1):
|
||||||
@@ -58,8 +58,6 @@ PODS:
|
|||||||
- photo_manager (3.7.1):
|
- photo_manager (3.7.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- pointer_interceptor_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- share_handler_ios (0.0.14):
|
- share_handler_ios (0.0.14):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -77,16 +75,16 @@ PODS:
|
|||||||
- sqflite_darwin (0.0.4):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (3.49.2):
|
- sqlite3 (3.49.1):
|
||||||
- sqlite3/common (= 3.49.2)
|
- sqlite3/common (= 3.49.1)
|
||||||
- sqlite3/common (3.49.2)
|
- sqlite3/common (3.49.1)
|
||||||
- sqlite3/dbstatvtab (3.49.2):
|
- sqlite3/dbstatvtab (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/fts5 (3.49.2):
|
- sqlite3/fts5 (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.49.2):
|
- sqlite3/perf-threadsafe (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.49.2):
|
- sqlite3/rtree (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -120,7 +118,7 @@ DEPENDENCIES:
|
|||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
|
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- maplibre_ios (from `.symlinks/plugins/maplibre_ios/ios`)
|
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
||||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||||
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
||||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
@@ -128,7 +126,6 @@ DEPENDENCIES:
|
|||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
|
||||||
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@@ -181,8 +178,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
|
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
maplibre_ios:
|
maplibre_gl:
|
||||||
:path: ".symlinks/plugins/maplibre_ios/ios"
|
:path: ".symlinks/plugins/maplibre_gl/ios"
|
||||||
native_video_player:
|
native_video_player:
|
||||||
:path: ".symlinks/plugins/native_video_player/ios"
|
:path: ".symlinks/plugins/native_video_player/ios"
|
||||||
network_info_plus:
|
network_info_plus:
|
||||||
@@ -197,8 +194,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
pointer_interceptor_ios:
|
|
||||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
|
||||||
share_handler_ios:
|
share_handler_ios:
|
||||||
:path: ".symlinks/plugins/share_handler_ios/ios"
|
:path: ".symlinks/plugins/share_handler_ios/ios"
|
||||||
share_handler_ios_models:
|
share_handler_ios_models:
|
||||||
@@ -235,8 +230,8 @@ SPEC CHECKSUMS:
|
|||||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||||
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
|
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
|
||||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||||
MapLibre: c0fcafabb341f230657d959970c6eb47fb55750e
|
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
||||||
maplibre_ios: 05031d5f79702672d2c01cc77b6ba3187d4bf896
|
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
||||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||||
@@ -244,14 +239,13 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
|
|||||||
@@ -446,7 +446,6 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||||
A1B2C3D4E5F6A7B8C9D0E1F2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */,
|
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
@@ -1251,14 +1250,6 @@
|
|||||||
minimumVersion = 1.5.0;
|
minimumVersion = 1.5.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
A1B2C3D4E5F6A7B8C9D0E1F2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/maplibre/maplibre-gl-native-distribution";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMinorVersion;
|
|
||||||
minimumVersion = 6.21.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
|||||||
@@ -10,15 +10,6 @@
|
|||||||
"version" : "1.0.3"
|
"version" : "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "maplibre-gl-native-distribution",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "2aefb4dd47ca6e897c93086f348a457839aac2fe",
|
|
||||||
"version" : "6.23.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "grdb.swift",
|
"identity" : "grdb.swift",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -18,3 +18,5 @@ enum ActionSource { timeline, viewer }
|
|||||||
enum CleanupStep { selectDate, scan, delete }
|
enum CleanupStep { selectDate, scan, delete }
|
||||||
|
|
||||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||||
|
|
||||||
|
enum AssetDateAggregation { start, end }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class Marker {
|
class Marker {
|
||||||
final Geographic location;
|
final LatLng location;
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
|
||||||
const Marker({required this.location, required this.assetId});
|
const Marker({required this.location, required this.assetId});
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ enum StoreKey<T> {
|
|||||||
autoPlayVideo<bool>._(139),
|
autoPlayVideo<bool>._(139),
|
||||||
albumGridView<bool>._(140),
|
albumGridView<bool>._(140),
|
||||||
|
|
||||||
|
// Image viewer navigation settings
|
||||||
|
tapToNavigate<bool>._(141),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000),
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
betaPromptShown<bool>._(1001),
|
betaPromptShown<bool>._(1001),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:immich_mobile/domain/models/map.model.dart';
|
import 'package:immich_mobile/domain/models/map.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:maplibre/maplibre.dart' hide Marker;
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
typedef MapMarkerSource = Future<List<Marker>> Function(LngLatBounds? bounds);
|
typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds);
|
||||||
|
|
||||||
typedef MapQuery = ({MapMarkerSource markerSource});
|
typedef MapQuery = ({MapMarkerSource markerSource});
|
||||||
|
|
||||||
@@ -21,5 +21,5 @@ class MapService {
|
|||||||
|
|
||||||
MapService(MapQuery query) : _markerSource = query.markerSource;
|
MapService(MapQuery query) : _markerSource = query.markerSource;
|
||||||
|
|
||||||
Future<List<Marker>> Function(LngLatBounds? bounds) get getMarkers => _markerSource;
|
Future<List<Marker>> Function(LatLngBounds? bounds) get getMarkers => _markerSource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class RemoteAlbumService {
|
|||||||
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||||
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
||||||
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
||||||
AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end),
|
||||||
AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start),
|
||||||
};
|
};
|
||||||
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
||||||
|
|
||||||
@@ -172,46 +172,25 @@ class RemoteAlbumService {
|
|||||||
return _repository.getAlbumsContainingAsset(assetId);
|
return _repository.getAlbumsContainingAsset(assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
Future<List<RemoteAlbum>> _sortByAssetDate(
|
||||||
// map album IDs to their newest asset dates
|
List<RemoteAlbum> albums, {
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
required AssetDateAggregation aggregation,
|
||||||
for (final album in albums) {
|
}) async {
|
||||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
|
if (albums.isEmpty) return [];
|
||||||
|
|
||||||
|
final albumIds = albums.map((e) => e.id).toList();
|
||||||
|
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
|
||||||
|
|
||||||
|
final albumMap = Map<String, RemoteAlbum>.fromEntries(albums.map((a) => MapEntry(a.id, a)));
|
||||||
|
|
||||||
|
final sortedAlbums = sortedIds.map((id) => albumMap[id]).whereType<RemoteAlbum>().toList();
|
||||||
|
|
||||||
|
if (sortedAlbums.length < albums.length) {
|
||||||
|
final returnedIdSet = sortedIds.toSet();
|
||||||
|
final emptyAlbums = albums.where((a) => !returnedIdSet.contains(a.id));
|
||||||
|
sortedAlbums.addAll(emptyAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
// await all database queries
|
return sortedAlbums;
|
||||||
final entries = await Future.wait(
|
|
||||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
|
||||||
);
|
|
||||||
final assetTimestamps = Map.fromEntries(entries);
|
|
||||||
|
|
||||||
final sorted = albums.sorted((a, b) {
|
|
||||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
return aDate.compareTo(bDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByOldestAsset(List<RemoteAlbum> albums) async {
|
|
||||||
// map album IDs to their oldest asset dates
|
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {
|
|
||||||
for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id),
|
|
||||||
};
|
|
||||||
|
|
||||||
// await all database queries
|
|
||||||
final entries = await Future.wait(
|
|
||||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
|
||||||
);
|
|
||||||
final assetTimestamps = Map.fromEntries(entries);
|
|
||||||
|
|
||||||
final sorted = albums.sorted((a, b) {
|
|
||||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
return aDate.compareTo(bDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ class SyncStreamService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
||||||
|
|
||||||
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
|
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
|
||||||
final migrations = (jsonDecode(value) as List).cast<String>();
|
final migrations = (jsonDecode(value) as List).cast<String>();
|
||||||
int previousLength = migrations.length;
|
int previousLength = migrations.length;
|
||||||
await _runPreSyncTasks(migrations, semVer);
|
await _runPreSyncTasks(migrations, serverSemVer);
|
||||||
|
|
||||||
if (migrations.length != previousLength) {
|
if (migrations.length != previousLength) {
|
||||||
_logger.info("Updated pre-sync migration status: $migrations");
|
_logger.info("Updated pre-sync migration status: $migrations");
|
||||||
@@ -82,10 +82,14 @@ class SyncStreamService {
|
|||||||
|
|
||||||
// Start the sync stream and handle events
|
// Start the sync stream and handle events
|
||||||
bool shouldReset = false;
|
bool shouldReset = false;
|
||||||
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
|
await _syncApiRepository.streamChanges(
|
||||||
|
_handleEvents,
|
||||||
|
serverVersion: serverSemVer,
|
||||||
|
onReset: () => shouldReset = true,
|
||||||
|
);
|
||||||
if (shouldReset) {
|
if (shouldReset) {
|
||||||
_logger.info("Resetting sync state as requested by server");
|
_logger.info("Resetting sync state as requested by server");
|
||||||
await _syncApiRepository.streamChanges(_handleEvents);
|
await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousLength = migrations.length;
|
previousLength = migrations.length;
|
||||||
@@ -282,6 +286,8 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.deletePeopleV1(data.cast());
|
return _syncStreamRepository.deletePeopleV1(data.cast());
|
||||||
case SyncEntityType.assetFaceV1:
|
case SyncEntityType.assetFaceV1:
|
||||||
return _syncStreamRepository.updateAssetFacesV1(data.cast());
|
return _syncStreamRepository.updateAssetFacesV1(data.cast());
|
||||||
|
case SyncEntityType.assetFaceV2:
|
||||||
|
return _syncStreamRepository.updateAssetFacesV2(data.cast());
|
||||||
case SyncEntityType.assetFaceDeleteV1:
|
case SyncEntityType.assetFaceDeleteV1:
|
||||||
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
return _syncStreamRepository.deleteAssetFacesV1(data.cast());
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
extension WithinBounds on LngLatBounds {
|
extension WithinBounds on LatLngBounds {
|
||||||
/// Checks whether [point] is inside bounds
|
/// Checks whether [point] is inside bounds
|
||||||
bool contains(Geographic point) {
|
bool contains(LatLng point) {
|
||||||
return containsBounds(
|
final sw = point;
|
||||||
LngLatBounds(
|
final ne = point;
|
||||||
longitudeWest: point.lon,
|
return containsBounds(LatLngBounds(southwest: sw, northeast: ne));
|
||||||
longitudeEast: point.lon,
|
|
||||||
latitudeSouth: point.lat,
|
|
||||||
latitudeNorth: point.lat,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether [bounds] is contained inside bounds
|
/// Checks whether [bounds] is contained inside bounds
|
||||||
bool containsBounds(LngLatBounds bounds) {
|
bool containsBounds(LatLngBounds bounds) {
|
||||||
return (bounds.latitudeSouth >= latitudeSouth) &&
|
final sw = bounds.southwest;
|
||||||
(bounds.latitudeNorth <= latitudeNorth) &&
|
final ne = bounds.northeast;
|
||||||
(bounds.longitudeWest >= longitudeWest) &&
|
return (sw.latitude >= southwest.latitude) &&
|
||||||
(bounds.longitudeEast <= longitudeEast);
|
(ne.latitude <= northeast.latitude) &&
|
||||||
|
(sw.longitude >= southwest.longitude) &&
|
||||||
|
(ne.longitude <= northeast.longitude);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
import 'package:immich_mobile/utils/map_utils.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
extension MapMarkers on MapController {
|
extension MapMarkers on MapLibreMapController {
|
||||||
static var _completer = Completer()..complete();
|
static var _completer = Completer()..complete();
|
||||||
|
|
||||||
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
|
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
|
||||||
return style!.addSource(
|
return addSource(
|
||||||
GeoJsonSource(
|
MapUtils.defaultSourceId,
|
||||||
id: MapUtils.defaultSourceId,
|
GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())),
|
||||||
data: jsonEncode(MapUtils.generateGeoJsonForMarkers(markers.toList())),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,28 +27,63 @@ extension MapMarkers on MapController {
|
|||||||
// !! Make sure to remove layers before sources else the native
|
// !! Make sure to remove layers before sources else the native
|
||||||
// maplibre library would crash when removing the source saying that
|
// maplibre library would crash when removing the source saying that
|
||||||
// the source is still in use
|
// the source is still in use
|
||||||
try {
|
final existingLayers = await getLayerIds();
|
||||||
await style!.removeLayer(MapUtils.defaultHeatMapLayerId);
|
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
|
||||||
} catch (_) {
|
await removeLayer(MapUtils.defaultHeatMapLayerId);
|
||||||
// Layer may not exist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
final existingSources = await getSourceIds();
|
||||||
await style!.removeSource(MapUtils.defaultSourceId);
|
if (existingSources.contains(MapUtils.defaultSourceId)) {
|
||||||
} catch (_) {
|
await removeSource(MapUtils.defaultSourceId);
|
||||||
// Source may not exist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await addGeoJSONSourceForMarkers(markers);
|
await addGeoJSONSourceForMarkers(markers);
|
||||||
|
|
||||||
await style!.addLayer(
|
if (Platform.isAndroid) {
|
||||||
const HeatmapStyleLayer(
|
await addCircleLayer(
|
||||||
id: MapUtils.defaultHeatMapLayerId,
|
MapUtils.defaultSourceId,
|
||||||
sourceId: MapUtils.defaultSourceId,
|
MapUtils.defaultHeatMapLayerId,
|
||||||
paint: MapUtils.defaultHeatMapLayerPaint,
|
const CircleLayerProperties(
|
||||||
|
circleRadius: 10,
|
||||||
|
circleColor: "rgba(150,86,34,0.7)",
|
||||||
|
circleBlur: 1.0,
|
||||||
|
circleOpacity: 0.7,
|
||||||
|
circleStrokeWidth: 0.1,
|
||||||
|
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||||
|
circleStrokeOpacity: 0.7,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
await addHeatmapLayer(
|
||||||
|
MapUtils.defaultSourceId,
|
||||||
|
MapUtils.defaultHeatMapLayerId,
|
||||||
|
MapUtils.defaultHeatMapLayerProperties,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_completer.complete();
|
_completer.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Symbol?> addMarkerAtLatLng(LatLng centre) async {
|
||||||
|
// no marker is displayed if asset-path is incorrect
|
||||||
|
try {
|
||||||
|
final ByteData bytes = await rootBundle.load("assets/location-pin.png");
|
||||||
|
await addImage("mapMarker", bytes.buffer.asUint8List());
|
||||||
|
return addSymbol(SymbolOptions(geometry: centre, iconImage: "mapMarker", iconSize: 0.15, iconAnchor: "bottom"));
|
||||||
|
} finally {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LatLngBounds> getBoundsFromPoint(Point<double> point, double distance) async {
|
||||||
|
final southWestPx = Point(point.x - distance, point.y + distance);
|
||||||
|
final northEastPx = Point(point.x + distance, point.y - distance);
|
||||||
|
|
||||||
|
final southWest = await toLatLng(southWestPx);
|
||||||
|
final northEast = await toLatLng(northEastPx);
|
||||||
|
|
||||||
|
return LatLngBounds(southwest: southWest, northeast: northEast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin {
|
|||||||
|
|
||||||
TextColumn get sourceType => text()();
|
TextColumn get sourceType => text()();
|
||||||
|
|
||||||
|
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
|
||||||
|
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-68
@@ -5,11 +5,12 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da
|
|||||||
as i1;
|
as i1;
|
||||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'
|
||||||
as i2;
|
as i2;
|
||||||
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
as i3;
|
as i4;
|
||||||
import 'package:drift/internal/modular.dart' as i4;
|
import 'package:drift/internal/modular.dart' as i5;
|
||||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||||
as i5;
|
as i6;
|
||||||
|
|
||||||
typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
||||||
i1.AssetFaceEntityCompanion Function({
|
i1.AssetFaceEntityCompanion Function({
|
||||||
@@ -23,6 +24,8 @@ typedef $$AssetFaceEntityTableCreateCompanionBuilder =
|
|||||||
required int boundingBoxX2,
|
required int boundingBoxX2,
|
||||||
required int boundingBoxY2,
|
required int boundingBoxY2,
|
||||||
required String sourceType,
|
required String sourceType,
|
||||||
|
i0.Value<bool> isVisible,
|
||||||
|
i0.Value<DateTime?> deletedAt,
|
||||||
});
|
});
|
||||||
typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
||||||
i1.AssetFaceEntityCompanion Function({
|
i1.AssetFaceEntityCompanion Function({
|
||||||
@@ -36,6 +39,8 @@ typedef $$AssetFaceEntityTableUpdateCompanionBuilder =
|
|||||||
i0.Value<int> boundingBoxX2,
|
i0.Value<int> boundingBoxX2,
|
||||||
i0.Value<int> boundingBoxY2,
|
i0.Value<int> boundingBoxY2,
|
||||||
i0.Value<String> sourceType,
|
i0.Value<String> sourceType,
|
||||||
|
i0.Value<bool> isVisible,
|
||||||
|
i0.Value<DateTime?> deletedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$AssetFaceEntityTableReferences
|
final class $$AssetFaceEntityTableReferences
|
||||||
@@ -51,29 +56,29 @@ final class $$AssetFaceEntityTableReferences
|
|||||||
super.$_typedResult,
|
super.$_typedResult,
|
||||||
);
|
);
|
||||||
|
|
||||||
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
.resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
.createAlias(
|
.createAlias(
|
||||||
i0.$_aliasNameGenerator(
|
i0.$_aliasNameGenerator(
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
||||||
.assetId,
|
.assetId,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
db,
|
db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
i4.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||||
final $_column = $_itemColumn<String>('asset_id')!;
|
final $_column = $_itemColumn<String>('asset_id')!;
|
||||||
|
|
||||||
final manager = i3
|
final manager = i4
|
||||||
.$$RemoteAssetEntityTableTableManager(
|
.$$RemoteAssetEntityTableTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
$_db,
|
$_db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
)
|
)
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||||
@@ -83,29 +88,29 @@ final class $$AssetFaceEntityTableReferences
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
|
static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) =>
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i5.$PersonEntityTable>('person_entity')
|
.resultSet<i6.$PersonEntityTable>('person_entity')
|
||||||
.createAlias(
|
.createAlias(
|
||||||
i0.$_aliasNameGenerator(
|
i0.$_aliasNameGenerator(
|
||||||
i4.ReadDatabaseContainer(db)
|
i5.ReadDatabaseContainer(db)
|
||||||
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
.resultSet<i1.$AssetFaceEntityTable>('asset_face_entity')
|
||||||
.personId,
|
.personId,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
db,
|
db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity').id,
|
).resultSet<i6.$PersonEntityTable>('person_entity').id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
i5.$$PersonEntityTableProcessedTableManager? get personId {
|
i6.$$PersonEntityTableProcessedTableManager? get personId {
|
||||||
final $_column = $_itemColumn<String>('person_id');
|
final $_column = $_itemColumn<String>('person_id');
|
||||||
if ($_column == null) return null;
|
if ($_column == null) return null;
|
||||||
final manager = i5
|
final manager = i6
|
||||||
.$$PersonEntityTableTableManager(
|
.$$PersonEntityTableTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i4.ReadDatabaseContainer(
|
i5.ReadDatabaseContainer(
|
||||||
$_db,
|
$_db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
)
|
)
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_personIdTable($_db));
|
final item = $_typedResult.readTableOrNull(_personIdTable($_db));
|
||||||
@@ -165,24 +170,34 @@ class $$AssetFaceEntityTableFilterComposer
|
|||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
|
i0.ColumnFilters<bool> get isVisible => $composableBuilder(
|
||||||
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
column: $table.isVisible,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
|
||||||
|
column: $table.deletedAt,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i4.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||||
|
final i4.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.assetId,
|
getCurrentColumn: (t) => t.assetId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$$RemoteAssetEntityTableFilterComposer(
|
}) => i4.$$RemoteAssetEntityTableFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -192,24 +207,24 @@ class $$AssetFaceEntityTableFilterComposer
|
|||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i5.$$PersonEntityTableFilterComposer get personId {
|
i6.$$PersonEntityTableFilterComposer get personId {
|
||||||
final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder(
|
final i6.$$PersonEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.personId,
|
getCurrentColumn: (t) => t.personId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i5.$$PersonEntityTableFilterComposer(
|
}) => i6.$$PersonEntityTableFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -269,25 +284,35 @@ class $$AssetFaceEntityTableOrderingComposer
|
|||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
i0.ColumnOrderings<bool> get isVisible => $composableBuilder(
|
||||||
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
|
column: $table.isVisible,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get deletedAt => $composableBuilder(
|
||||||
|
column: $table.deletedAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i4.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||||
|
final i4.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||||
$composerBuilder(
|
$composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.assetId,
|
getCurrentColumn: (t) => t.assetId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$$RemoteAssetEntityTableOrderingComposer(
|
}) => i4.$$RemoteAssetEntityTableOrderingComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -297,24 +322,24 @@ class $$AssetFaceEntityTableOrderingComposer
|
|||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i5.$$PersonEntityTableOrderingComposer get personId {
|
i6.$$PersonEntityTableOrderingComposer get personId {
|
||||||
final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
|
final i6.$$PersonEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.personId,
|
getCurrentColumn: (t) => t.personId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i5.$$PersonEntityTableOrderingComposer(
|
}) => i6.$$PersonEntityTableOrderingComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -372,25 +397,31 @@ class $$AssetFaceEntityTableAnnotationComposer
|
|||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
i0.GeneratedColumn<bool> get isVisible =>
|
||||||
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
|
$composableBuilder(column: $table.isVisible, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i4.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||||
|
final i4.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||||
$composerBuilder(
|
$composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.assetId,
|
getCurrentColumn: (t) => t.assetId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$$RemoteAssetEntityTableAnnotationComposer(
|
}) => i4.$$RemoteAssetEntityTableAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
).resultSet<i4.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -400,24 +431,24 @@ class $$AssetFaceEntityTableAnnotationComposer
|
|||||||
return composer;
|
return composer;
|
||||||
}
|
}
|
||||||
|
|
||||||
i5.$$PersonEntityTableAnnotationComposer get personId {
|
i6.$$PersonEntityTableAnnotationComposer get personId {
|
||||||
final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
|
final i6.$$PersonEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.personId,
|
getCurrentColumn: (t) => t.personId,
|
||||||
referencedTable: i4.ReadDatabaseContainer(
|
referencedTable: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
joinBuilder, {
|
joinBuilder, {
|
||||||
$addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i5.$$PersonEntityTableAnnotationComposer(
|
}) => i6.$$PersonEntityTableAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i4.ReadDatabaseContainer(
|
$table: i5.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i5.$PersonEntityTable>('person_entity'),
|
).resultSet<i6.$PersonEntityTable>('person_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -468,6 +499,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
i0.Value<int> boundingBoxX2 = const i0.Value.absent(),
|
i0.Value<int> boundingBoxX2 = const i0.Value.absent(),
|
||||||
i0.Value<int> boundingBoxY2 = const i0.Value.absent(),
|
i0.Value<int> boundingBoxY2 = const i0.Value.absent(),
|
||||||
i0.Value<String> sourceType = const i0.Value.absent(),
|
i0.Value<String> sourceType = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
}) => i1.AssetFaceEntityCompanion(
|
}) => i1.AssetFaceEntityCompanion(
|
||||||
id: id,
|
id: id,
|
||||||
assetId: assetId,
|
assetId: assetId,
|
||||||
@@ -479,6 +512,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
boundingBoxX2: boundingBoxX2,
|
boundingBoxX2: boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2,
|
boundingBoxY2: boundingBoxY2,
|
||||||
sourceType: sourceType,
|
sourceType: sourceType,
|
||||||
|
isVisible: isVisible,
|
||||||
|
deletedAt: deletedAt,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
@@ -492,6 +527,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
required int boundingBoxX2,
|
required int boundingBoxX2,
|
||||||
required int boundingBoxY2,
|
required int boundingBoxY2,
|
||||||
required String sourceType,
|
required String sourceType,
|
||||||
|
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
}) => i1.AssetFaceEntityCompanion.insert(
|
}) => i1.AssetFaceEntityCompanion.insert(
|
||||||
id: id,
|
id: id,
|
||||||
assetId: assetId,
|
assetId: assetId,
|
||||||
@@ -503,6 +540,8 @@ class $$AssetFaceEntityTableTableManager
|
|||||||
boundingBoxX2: boundingBoxX2,
|
boundingBoxX2: boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2,
|
boundingBoxY2: boundingBoxY2,
|
||||||
sourceType: sourceType,
|
sourceType: sourceType,
|
||||||
|
isVisible: isVisible,
|
||||||
|
deletedAt: deletedAt,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map(
|
.map(
|
||||||
@@ -709,6 +748,33 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
type: i0.DriftSqlType.string,
|
type: i0.DriftSqlType.string,
|
||||||
requiredDuringInsert: true,
|
requiredDuringInsert: true,
|
||||||
);
|
);
|
||||||
|
static const i0.VerificationMeta _isVisibleMeta = const i0.VerificationMeta(
|
||||||
|
'isVisible',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> isVisible = i0.GeneratedColumn<bool>(
|
||||||
|
'is_visible',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_visible" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const i3.Constant(true),
|
||||||
|
);
|
||||||
|
static const i0.VerificationMeta _deletedAtMeta = const i0.VerificationMeta(
|
||||||
|
'deletedAt',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> deletedAt =
|
||||||
|
i0.GeneratedColumn<DateTime>(
|
||||||
|
'deleted_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
id,
|
id,
|
||||||
@@ -721,6 +787,8 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
boundingBoxX2,
|
boundingBoxX2,
|
||||||
boundingBoxY2,
|
boundingBoxY2,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
isVisible,
|
||||||
|
deletedAt,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -824,6 +892,18 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_sourceTypeMeta);
|
context.missing(_sourceTypeMeta);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('is_visible')) {
|
||||||
|
context.handle(
|
||||||
|
_isVisibleMeta,
|
||||||
|
isVisible.isAcceptableOrUnknown(data['is_visible']!, _isVisibleMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('deleted_at')) {
|
||||||
|
context.handle(
|
||||||
|
_deletedAtMeta,
|
||||||
|
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,6 +953,14 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity
|
|||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}source_type'],
|
data['${effectivePrefix}source_type'],
|
||||||
)!,
|
)!,
|
||||||
|
isVisible: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_visible'],
|
||||||
|
)!,
|
||||||
|
deletedAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime,
|
||||||
|
data['${effectivePrefix}deleted_at'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,6 +987,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
final int boundingBoxX2;
|
final int boundingBoxX2;
|
||||||
final int boundingBoxY2;
|
final int boundingBoxY2;
|
||||||
final String sourceType;
|
final String sourceType;
|
||||||
|
final bool isVisible;
|
||||||
|
final DateTime? deletedAt;
|
||||||
const AssetFaceEntityData({
|
const AssetFaceEntityData({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
@@ -910,6 +1000,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
required this.boundingBoxX2,
|
required this.boundingBoxX2,
|
||||||
required this.boundingBoxY2,
|
required this.boundingBoxY2,
|
||||||
required this.sourceType,
|
required this.sourceType,
|
||||||
|
required this.isVisible,
|
||||||
|
this.deletedAt,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
@@ -926,6 +1018,10 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2);
|
map['bounding_box_x2'] = i0.Variable<int>(boundingBoxX2);
|
||||||
map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2);
|
map['bounding_box_y2'] = i0.Variable<int>(boundingBoxY2);
|
||||||
map['source_type'] = i0.Variable<String>(sourceType);
|
map['source_type'] = i0.Variable<String>(sourceType);
|
||||||
|
map['is_visible'] = i0.Variable<bool>(isVisible);
|
||||||
|
if (!nullToAbsent || deletedAt != null) {
|
||||||
|
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,6 +1041,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']),
|
boundingBoxX2: serializer.fromJson<int>(json['boundingBoxX2']),
|
||||||
boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']),
|
boundingBoxY2: serializer.fromJson<int>(json['boundingBoxY2']),
|
||||||
sourceType: serializer.fromJson<String>(json['sourceType']),
|
sourceType: serializer.fromJson<String>(json['sourceType']),
|
||||||
|
isVisible: serializer.fromJson<bool>(json['isVisible']),
|
||||||
|
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -961,6 +1059,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
'boundingBoxX2': serializer.toJson<int>(boundingBoxX2),
|
'boundingBoxX2': serializer.toJson<int>(boundingBoxX2),
|
||||||
'boundingBoxY2': serializer.toJson<int>(boundingBoxY2),
|
'boundingBoxY2': serializer.toJson<int>(boundingBoxY2),
|
||||||
'sourceType': serializer.toJson<String>(sourceType),
|
'sourceType': serializer.toJson<String>(sourceType),
|
||||||
|
'isVisible': serializer.toJson<bool>(isVisible),
|
||||||
|
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,6 +1075,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
int? boundingBoxX2,
|
int? boundingBoxX2,
|
||||||
int? boundingBoxY2,
|
int? boundingBoxY2,
|
||||||
String? sourceType,
|
String? sourceType,
|
||||||
|
bool? isVisible,
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
}) => i1.AssetFaceEntityData(
|
}) => i1.AssetFaceEntityData(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
assetId: assetId ?? this.assetId,
|
assetId: assetId ?? this.assetId,
|
||||||
@@ -986,6 +1088,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
||||||
sourceType: sourceType ?? this.sourceType,
|
sourceType: sourceType ?? this.sourceType,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
|
||||||
);
|
);
|
||||||
AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) {
|
AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) {
|
||||||
return AssetFaceEntityData(
|
return AssetFaceEntityData(
|
||||||
@@ -1013,6 +1117,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
sourceType: data.sourceType.present
|
sourceType: data.sourceType.present
|
||||||
? data.sourceType.value
|
? data.sourceType.value
|
||||||
: this.sourceType,
|
: this.sourceType,
|
||||||
|
isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible,
|
||||||
|
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,7 +1134,9 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
..write('boundingBoxY1: $boundingBoxY1, ')
|
..write('boundingBoxY1: $boundingBoxY1, ')
|
||||||
..write('boundingBoxX2: $boundingBoxX2, ')
|
..write('boundingBoxX2: $boundingBoxX2, ')
|
||||||
..write('boundingBoxY2: $boundingBoxY2, ')
|
..write('boundingBoxY2: $boundingBoxY2, ')
|
||||||
..write('sourceType: $sourceType')
|
..write('sourceType: $sourceType, ')
|
||||||
|
..write('isVisible: $isVisible, ')
|
||||||
|
..write('deletedAt: $deletedAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@@ -1045,6 +1153,8 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
boundingBoxX2,
|
boundingBoxX2,
|
||||||
boundingBoxY2,
|
boundingBoxY2,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
isVisible,
|
||||||
|
deletedAt,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -1059,7 +1169,9 @@ class AssetFaceEntityData extends i0.DataClass
|
|||||||
other.boundingBoxY1 == this.boundingBoxY1 &&
|
other.boundingBoxY1 == this.boundingBoxY1 &&
|
||||||
other.boundingBoxX2 == this.boundingBoxX2 &&
|
other.boundingBoxX2 == this.boundingBoxX2 &&
|
||||||
other.boundingBoxY2 == this.boundingBoxY2 &&
|
other.boundingBoxY2 == this.boundingBoxY2 &&
|
||||||
other.sourceType == this.sourceType);
|
other.sourceType == this.sourceType &&
|
||||||
|
other.isVisible == this.isVisible &&
|
||||||
|
other.deletedAt == this.deletedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetFaceEntityCompanion
|
class AssetFaceEntityCompanion
|
||||||
@@ -1074,6 +1186,8 @@ class AssetFaceEntityCompanion
|
|||||||
final i0.Value<int> boundingBoxX2;
|
final i0.Value<int> boundingBoxX2;
|
||||||
final i0.Value<int> boundingBoxY2;
|
final i0.Value<int> boundingBoxY2;
|
||||||
final i0.Value<String> sourceType;
|
final i0.Value<String> sourceType;
|
||||||
|
final i0.Value<bool> isVisible;
|
||||||
|
final i0.Value<DateTime?> deletedAt;
|
||||||
const AssetFaceEntityCompanion({
|
const AssetFaceEntityCompanion({
|
||||||
this.id = const i0.Value.absent(),
|
this.id = const i0.Value.absent(),
|
||||||
this.assetId = const i0.Value.absent(),
|
this.assetId = const i0.Value.absent(),
|
||||||
@@ -1085,6 +1199,8 @@ class AssetFaceEntityCompanion
|
|||||||
this.boundingBoxX2 = const i0.Value.absent(),
|
this.boundingBoxX2 = const i0.Value.absent(),
|
||||||
this.boundingBoxY2 = const i0.Value.absent(),
|
this.boundingBoxY2 = const i0.Value.absent(),
|
||||||
this.sourceType = const i0.Value.absent(),
|
this.sourceType = const i0.Value.absent(),
|
||||||
|
this.isVisible = const i0.Value.absent(),
|
||||||
|
this.deletedAt = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
AssetFaceEntityCompanion.insert({
|
AssetFaceEntityCompanion.insert({
|
||||||
required String id,
|
required String id,
|
||||||
@@ -1097,6 +1213,8 @@ class AssetFaceEntityCompanion
|
|||||||
required int boundingBoxX2,
|
required int boundingBoxX2,
|
||||||
required int boundingBoxY2,
|
required int boundingBoxY2,
|
||||||
required String sourceType,
|
required String sourceType,
|
||||||
|
this.isVisible = const i0.Value.absent(),
|
||||||
|
this.deletedAt = const i0.Value.absent(),
|
||||||
}) : id = i0.Value(id),
|
}) : id = i0.Value(id),
|
||||||
assetId = i0.Value(assetId),
|
assetId = i0.Value(assetId),
|
||||||
imageWidth = i0.Value(imageWidth),
|
imageWidth = i0.Value(imageWidth),
|
||||||
@@ -1117,6 +1235,8 @@ class AssetFaceEntityCompanion
|
|||||||
i0.Expression<int>? boundingBoxX2,
|
i0.Expression<int>? boundingBoxX2,
|
||||||
i0.Expression<int>? boundingBoxY2,
|
i0.Expression<int>? boundingBoxY2,
|
||||||
i0.Expression<String>? sourceType,
|
i0.Expression<String>? sourceType,
|
||||||
|
i0.Expression<bool>? isVisible,
|
||||||
|
i0.Expression<DateTime>? deletedAt,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
@@ -1129,6 +1249,8 @@ class AssetFaceEntityCompanion
|
|||||||
if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2,
|
if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2,
|
||||||
if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2,
|
if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2,
|
||||||
if (sourceType != null) 'source_type': sourceType,
|
if (sourceType != null) 'source_type': sourceType,
|
||||||
|
if (isVisible != null) 'is_visible': isVisible,
|
||||||
|
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,6 +1265,8 @@ class AssetFaceEntityCompanion
|
|||||||
i0.Value<int>? boundingBoxX2,
|
i0.Value<int>? boundingBoxX2,
|
||||||
i0.Value<int>? boundingBoxY2,
|
i0.Value<int>? boundingBoxY2,
|
||||||
i0.Value<String>? sourceType,
|
i0.Value<String>? sourceType,
|
||||||
|
i0.Value<bool>? isVisible,
|
||||||
|
i0.Value<DateTime?>? deletedAt,
|
||||||
}) {
|
}) {
|
||||||
return i1.AssetFaceEntityCompanion(
|
return i1.AssetFaceEntityCompanion(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -1155,6 +1279,8 @@ class AssetFaceEntityCompanion
|
|||||||
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2,
|
||||||
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2,
|
||||||
sourceType: sourceType ?? this.sourceType,
|
sourceType: sourceType ?? this.sourceType,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,6 +1317,12 @@ class AssetFaceEntityCompanion
|
|||||||
if (sourceType.present) {
|
if (sourceType.present) {
|
||||||
map['source_type'] = i0.Variable<String>(sourceType.value);
|
map['source_type'] = i0.Variable<String>(sourceType.value);
|
||||||
}
|
}
|
||||||
|
if (isVisible.present) {
|
||||||
|
map['is_visible'] = i0.Variable<bool>(isVisible.value);
|
||||||
|
}
|
||||||
|
if (deletedAt.present) {
|
||||||
|
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,7 +1338,9 @@ class AssetFaceEntityCompanion
|
|||||||
..write('boundingBoxY1: $boundingBoxY1, ')
|
..write('boundingBoxY1: $boundingBoxY1, ')
|
||||||
..write('boundingBoxX2: $boundingBoxX2, ')
|
..write('boundingBoxX2: $boundingBoxX2, ')
|
||||||
..write('boundingBoxY2: $boundingBoxY2, ')
|
..write('boundingBoxY2: $boundingBoxY2, ')
|
||||||
..write('sourceType: $sourceType')
|
..write('sourceType: $sourceType, ')
|
||||||
|
..write('isVisible: $isVisible, ')
|
||||||
|
..write('deletedAt: $deletedAt')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// ignore_for_file: experimental_member_use
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
@@ -98,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 19;
|
int get schemaVersion => 20;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -227,6 +226,10 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
||||||
await m.createIndex(v19.idxStackPrimaryAssetId);
|
await m.createIndex(v19.idxStackPrimaryAssetId);
|
||||||
},
|
},
|
||||||
|
from19To20: (m, v20) async {
|
||||||
|
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
||||||
|
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8360,6 +8360,550 @@ final class Schema19 extends i0.VersionedSchema {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class Schema20 extends i0.VersionedSchema {
|
||||||
|
Schema20({required super.database}) : super(version: 20);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAlbumOwnerId,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxLocalAssetCloudId,
|
||||||
|
idxStackPrimaryAssetId,
|
||||||
|
idxRemoteAssetOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
idxRemoteAssetStackId,
|
||||||
|
idxRemoteAssetLocalDateTimeDay,
|
||||||
|
idxRemoteAssetLocalDateTimeMonth,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
remoteAssetCloudIdEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
idxPartnerSharedWithId,
|
||||||
|
idxLatLng,
|
||||||
|
idxRemoteAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAssetCloudId,
|
||||||
|
idxPersonOwnerId,
|
||||||
|
idxAssetFacePersonId,
|
||||||
|
idxAssetFaceAssetId,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
];
|
||||||
|
late final Shape20 userEntity = Shape20(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_91,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape28 remoteAssetEntity = Shape28(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_86,
|
||||||
|
_column_101,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape26 localAssetEntity = Shape26(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_98,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape19 localAlbumEntity = Shape19(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_90,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_34, _column_35, _column_33],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_local_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumOwnerId = i1.Index(
|
||||||
|
'idx_remote_album_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||||
|
'idx_local_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||||
|
'idx_stack_primary_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_owner_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||||
|
'idx_remote_asset_stack_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
|
||||||
|
'idx_remote_asset_local_date_time_day',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
|
||||||
|
'idx_remote_asset_local_date_time_month',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
||||||
|
);
|
||||||
|
late final Shape21 authUserEntity = Shape21(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_2,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_92,
|
||||||
|
_column_93,
|
||||||
|
_column_7,
|
||||||
|
_column_94,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_25, _column_26, _column_27],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_28, _column_29, _column_30],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_36, _column_60],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||||
|
columns: [_column_60, _column_25, _column_61],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape27 remoteAssetCloudIdEntity = Shape27(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_cloud_id_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_99,
|
||||||
|
_column_100,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||||
|
columns: [_column_36, _column_68],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape14 personEntity = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape29 assetFaceEntity = Shape29(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_36,
|
||||||
|
_column_76,
|
||||||
|
_column_77,
|
||||||
|
_column_78,
|
||||||
|
_column_79,
|
||||||
|
_column_80,
|
||||||
|
_column_81,
|
||||||
|
_column_82,
|
||||||
|
_column_83,
|
||||||
|
_column_102,
|
||||||
|
_column_18,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_87, _column_88, _column_89],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape25 trashedLocalAssetEntity = Shape25(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_95,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_97,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||||
|
'idx_partner_shared_with_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_remote_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||||
|
'idx_remote_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxPersonOwnerId = i1.Index(
|
||||||
|
'idx_person_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||||
|
'idx_asset_face_person_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||||
|
'idx_asset_face_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape29 extends i0.VersionedTable {
|
||||||
|
Shape29({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get personId =>
|
||||||
|
columnsByName['person_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get imageWidth =>
|
||||||
|
columnsByName['image_width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get imageHeight =>
|
||||||
|
columnsByName['image_height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get boundingBoxX1 =>
|
||||||
|
columnsByName['bounding_box_x1']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get boundingBoxY1 =>
|
||||||
|
columnsByName['bounding_box_y1']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get boundingBoxX2 =>
|
||||||
|
columnsByName['bounding_box_x2']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get boundingBoxY2 =>
|
||||||
|
columnsByName['bounding_box_y2']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get sourceType =>
|
||||||
|
columnsByName['source_type']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isVisible =>
|
||||||
|
columnsByName['is_visible']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<bool> _column_102(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>(
|
||||||
|
'is_visible',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_visible" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const CustomExpression('1'),
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -8379,6 +8923,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -8472,6 +9017,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from18To19(migrator, schema);
|
await from18To19(migrator, schema);
|
||||||
return 19;
|
return 19;
|
||||||
|
case 19:
|
||||||
|
final schema = Schema20(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from19To20(migrator, schema);
|
||||||
|
return 20;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -8497,6 +9047,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -8517,5 +9068,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from16To17: from16To17,
|
from16To17: from16To17,
|
||||||
from17To18: from17To18,
|
from17To18: from17To18,
|
||||||
from18To19: from18To19,
|
from18To19: from18To19,
|
||||||
|
from19To20: from19To20,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keepFavorites) {
|
if (keepFavorites) {
|
||||||
whereClause = whereClause & _db.localAssetEntity.isFavorite.equals(false);
|
whereClause =
|
||||||
|
whereClause & _db.localAssetEntity.isFavorite.equals(false) & _db.remoteAssetEntity.isFavorite.equals(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
query.where(whereClause);
|
query.where(whereClause);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:maplibre/maplibre.dart' hide Marker;
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class DriftMapRepository extends DriftDatabaseRepository {
|
class DriftMapRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@@ -42,7 +42,7 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<List<Marker>> _watchMapMarker({
|
Future<List<Marker>> _watchMapMarker({
|
||||||
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter,
|
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter,
|
||||||
LngLatBounds? bounds,
|
LatLngBounds? bounds,
|
||||||
}) async {
|
}) async {
|
||||||
final assetId = _db.remoteExifEntity.assetId;
|
final assetId = _db.remoteExifEntity.assetId;
|
||||||
final latitude = _db.remoteExifEntity.latitude;
|
final latitude = _db.remoteExifEntity.latitude;
|
||||||
@@ -66,21 +66,20 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
|||||||
final rows = await query.get();
|
final rows = await query.get();
|
||||||
return List.generate(rows.length, (i) {
|
return List.generate(rows.length, (i) {
|
||||||
final row = rows[i];
|
final row = rows[i];
|
||||||
return Marker(
|
return Marker(assetId: row.read(assetId)!, location: LatLng(row.read(latitude)!, row.read(longitude)!));
|
||||||
assetId: row.read(assetId)!,
|
|
||||||
location: Geographic(lat: row.read(latitude)!, lon: row.read(longitude)!),
|
|
||||||
);
|
|
||||||
}, growable: false);
|
}, growable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MapBounds on $RemoteExifEntityTable {
|
extension MapBounds on $RemoteExifEntityTable {
|
||||||
Expression<bool> inBounds(LngLatBounds bounds) {
|
Expression<bool> inBounds(LatLngBounds bounds) {
|
||||||
final latInBounds = latitude.isBetweenValues(bounds.latitudeSouth, bounds.latitudeNorth);
|
final southwest = bounds.southwest;
|
||||||
final longInBounds = bounds.longitudeWest <= bounds.longitudeEast
|
final northeast = bounds.northeast;
|
||||||
? longitude.isBetweenValues(bounds.longitudeWest, bounds.longitudeEast)
|
|
||||||
: (longitude.isBiggerOrEqualValue(bounds.longitudeWest) |
|
final latInBounds = latitude.isBetweenValues(southwest.latitude, northeast.latitude);
|
||||||
longitude.isSmallerOrEqualValue(bounds.longitudeEast));
|
final longInBounds = southwest.longitude <= northeast.longitude
|
||||||
|
? longitude.isBetweenValues(southwest.longitude, northeast.longitude)
|
||||||
|
: (longitude.isBiggerOrEqualValue(southwest.longitude) | longitude.isSmallerOrEqualValue(northeast.longitude));
|
||||||
return latInBounds & longInBounds;
|
return latInBounds & longInBounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DriftPerson>> getAssetPeople(String assetId) async {
|
Future<List<DriftPerson>> getAssetPeople(String assetId) async {
|
||||||
final query = _db.select(_db.assetFaceEntity).join([
|
final query =
|
||||||
|
_db.select(_db.assetFaceEntity).join([
|
||||||
innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)),
|
innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)),
|
||||||
])..where(_db.assetFaceEntity.assetId.equals(assetId) & _db.personEntity.isHidden.equals(false));
|
])..where(
|
||||||
|
_db.assetFaceEntity.assetId.equals(assetId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull() &
|
||||||
|
_db.personEntity.isHidden.equals(false),
|
||||||
|
);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
final person = row.readTable(_db.personEntity);
|
final person = row.readTable(_db.personEntity);
|
||||||
@@ -39,7 +45,9 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
|||||||
..where(
|
..where(
|
||||||
people.isHidden.equals(false) &
|
people.isHidden.equals(false) &
|
||||||
assets.deletedAt.isNull() &
|
assets.deletedAt.isNull() &
|
||||||
assets.visibility.equalsValue(AssetVisibility.timeline),
|
assets.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
|
faces.isVisible.equals(true) &
|
||||||
|
faces.deletedAt.isNull(),
|
||||||
)
|
)
|
||||||
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not())
|
..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not())
|
||||||
..orderBy([
|
..orderBy([
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
@@ -321,26 +323,32 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async {
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
if (albumIds.isEmpty) return [];
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
|
|
||||||
..join([
|
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
final jsonIds = jsonEncode(albumIds);
|
||||||
}
|
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX';
|
||||||
|
|
||||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
final rows = await _db
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
.customSelect(
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
'''
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
SELECT
|
||||||
..join([
|
raae.album_id,
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
$sqlAgg(rae.local_date_time) AS asset_date
|
||||||
]);
|
FROM json_each(?) ids
|
||||||
|
INNER JOIN remote_album_asset_entity raae
|
||||||
|
ON raae.album_id = ids.value
|
||||||
|
INNER JOIN remote_asset_entity rae
|
||||||
|
ON rae.id = raae.asset_id
|
||||||
|
GROUP BY raae.album_id
|
||||||
|
ORDER BY asset_date ASC
|
||||||
|
''',
|
||||||
|
variables: [Variable<String>(jsonIds)],
|
||||||
|
readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity},
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
return rows.map((row) => row.read<String>('album_id')).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@@ -170,12 +170,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, Geographic location) {
|
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||||
return _db.batch((batch) async {
|
return _db.batch((batch) async {
|
||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
batch.update(
|
batch.update(
|
||||||
_db.remoteExifEntity,
|
_db.remoteExifEntity,
|
||||||
RemoteExifEntityCompanion(latitude: Value(location.lat), longitude: Value(location.lon)),
|
RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)),
|
||||||
where: (e) => e.assetId.equals(id),
|
where: (e) => e.assetId.equals(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ class SyncApiRepository {
|
|||||||
|
|
||||||
Future<void> streamChanges(
|
Future<void> streamChanges(
|
||||||
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
||||||
|
required SemVer serverVersion,
|
||||||
Function()? onReset,
|
Function()? onReset,
|
||||||
int batchSize = kSyncEventBatchSize,
|
int batchSize = kSyncEventBatchSize,
|
||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
@@ -64,7 +66,8 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.partnerStacksV1,
|
SyncRequestType.partnerStacksV1,
|
||||||
SyncRequestType.userMetadataV1,
|
SyncRequestType.userMetadataV1,
|
||||||
SyncRequestType.peopleV1,
|
SyncRequestType.peopleV1,
|
||||||
SyncRequestType.assetFacesV1,
|
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
|
||||||
|
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
|
||||||
],
|
],
|
||||||
reset: shouldReset,
|
reset: shouldReset,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
@@ -190,6 +193,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
SyncEntityType.personV1: SyncPersonV1.fromJson,
|
||||||
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
|
||||||
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
|
||||||
|
SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson,
|
||||||
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
|
||||||
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -652,6 +652,37 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateAssetFacesV2(Iterable<SyncAssetFaceV2> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final assetFace in data) {
|
||||||
|
final companion = AssetFaceEntityCompanion(
|
||||||
|
assetId: Value(assetFace.assetId),
|
||||||
|
personId: Value(assetFace.personId),
|
||||||
|
imageWidth: Value(assetFace.imageWidth),
|
||||||
|
imageHeight: Value(assetFace.imageHeight),
|
||||||
|
boundingBoxX1: Value(assetFace.boundingBoxX1),
|
||||||
|
boundingBoxY1: Value(assetFace.boundingBoxY1),
|
||||||
|
boundingBoxX2: Value(assetFace.boundingBoxX2),
|
||||||
|
boundingBoxY2: Value(assetFace.boundingBoxY2),
|
||||||
|
sourceType: Value(assetFace.sourceType),
|
||||||
|
deletedAt: Value(assetFace.deletedAt),
|
||||||
|
isVisible: Value(assetFace.isVisible),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.assetFaceEntity,
|
||||||
|
companion.copyWith(id: Value(assetFace.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: updateAssetFacesV2', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> data) async {
|
Future<void> deleteAssetFacesV1(Iterable<SyncAssetFaceDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:stream_transform/stream_transform.dart';
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
|
||||||
class TimelineMapOptions {
|
class TimelineMapOptions {
|
||||||
final LngLatBounds bounds;
|
final LatLngBounds bounds;
|
||||||
final bool onlyFavorites;
|
final bool onlyFavorites;
|
||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
@@ -323,6 +323,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
|
row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive),
|
||||||
groupBy: groupBy,
|
groupBy: groupBy,
|
||||||
origin: TimelineOrigin.archive,
|
origin: TimelineOrigin.archive,
|
||||||
|
joinLocal: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
|
||||||
@@ -421,7 +422,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.assetFaceEntity.personId.equals(personId),
|
_db.assetFaceEntity.personId.equals(personId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return query.map((row) {
|
return query.map((row) {
|
||||||
@@ -446,7 +449,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.assetFaceEntity.personId.equals(personId),
|
_db.assetFaceEntity.personId.equals(personId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
)
|
)
|
||||||
..groupBy([dateExp])
|
..groupBy([dateExp])
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
@@ -476,7 +481,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.assetFaceEntity.personId.equals(personId),
|
_db.assetFaceEntity.personId.equals(personId) &
|
||||||
|
_db.assetFaceEntity.isVisible.equals(true) &
|
||||||
|
_db.assetFaceEntity.deletedAt.isNull(),
|
||||||
)
|
)
|
||||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class MapMarker {
|
class MapMarker {
|
||||||
final Geographic latLng;
|
final LatLng latLng;
|
||||||
final String assetRemoteId;
|
final String assetRemoteId;
|
||||||
const MapMarker({required this.latLng, required this.assetRemoteId});
|
const MapMarker({required this.latLng, required this.assetRemoteId});
|
||||||
|
|
||||||
MapMarker copyWith({Geographic? latLng, String? assetRemoteId}) {
|
MapMarker copyWith({LatLng? latLng, String? assetRemoteId}) {
|
||||||
return MapMarker(latLng: latLng ?? this.latLng, assetRemoteId: assetRemoteId ?? this.assetRemoteId);
|
return MapMarker(latLng: latLng ?? this.latLng, assetRemoteId: assetRemoteId ?? this.assetRemoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
MapMarker.fromDto(MapMarkerResponseDto dto) : latLng = Geographic(lat: dto.lat, lon: dto.lon), assetRemoteId = dto.id;
|
MapMarker.fromDto(MapMarkerResponseDto dto) : latLng = LatLng(dto.lat, dto.lon), assetRemoteId = dto.id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)';
|
String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)';
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class SharedLink {
|
|||||||
final String key;
|
final String key;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
final SharedLinkSource type;
|
final SharedLinkSource type;
|
||||||
|
final String? slug;
|
||||||
|
|
||||||
const SharedLink({
|
const SharedLink({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -27,6 +28,7 @@ class SharedLink {
|
|||||||
required this.key,
|
required this.key,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
required this.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
SharedLink copyWith({
|
SharedLink copyWith({
|
||||||
@@ -41,6 +43,7 @@ class SharedLink {
|
|||||||
String? key,
|
String? key,
|
||||||
bool? showMetadata,
|
bool? showMetadata,
|
||||||
SharedLinkSource? type,
|
SharedLinkSource? type,
|
||||||
|
String? slug,
|
||||||
}) {
|
}) {
|
||||||
return SharedLink(
|
return SharedLink(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -54,6 +57,7 @@ class SharedLink {
|
|||||||
key: key ?? this.key,
|
key: key ?? this.key,
|
||||||
showMetadata: showMetadata ?? this.showMetadata,
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
slug: slug ?? this.slug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +70,7 @@ class SharedLink {
|
|||||||
expiresAt = dto.expiresAt,
|
expiresAt = dto.expiresAt,
|
||||||
key = dto.key,
|
key = dto.key,
|
||||||
showMetadata = dto.showMetadata,
|
showMetadata = dto.showMetadata,
|
||||||
|
slug = dto.slug,
|
||||||
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
|
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
|
||||||
title = dto.type == SharedLinkType.ALBUM
|
title = dto.type == SharedLinkType.ALBUM
|
||||||
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
||||||
@@ -78,7 +83,7 @@ class SharedLink {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
|
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type, slug=$slug)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -94,7 +99,8 @@ class SharedLink {
|
|||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.key == key &&
|
other.key == key &&
|
||||||
other.showMetadata == showMetadata &&
|
other.showMetadata == showMetadata &&
|
||||||
other.type == type;
|
other.type == type &&
|
||||||
|
other.slug == slug;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@@ -108,5 +114,6 @@ class SharedLink {
|
|||||||
expiresAt.hashCode ^
|
expiresAt.hashCode ^
|
||||||
key.hashCode ^
|
key.hashCode ^
|
||||||
showMetadata.hashCode ^
|
showMetadata.hashCode ^
|
||||||
type.hashCode;
|
type.hashCode ^
|
||||||
|
slug.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,8 +221,37 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
onDragUpdate: (_, details, __) {
|
onDragUpdate: (_, details, __) {
|
||||||
handleSwipeUpDown(details);
|
handleSwipeUpDown(details);
|
||||||
},
|
},
|
||||||
onTapDown: (_, __, ___) {
|
onTapDown: (ctx, tapDownDetails, _) {
|
||||||
|
final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.tapToNavigate);
|
||||||
|
if (!tapToNavigate) {
|
||||||
ref.read(showControlsProvider.notifier).toggle();
|
ref.read(showControlsProvider.notifier).toggle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double tapX = tapDownDetails.globalPosition.dx;
|
||||||
|
double screenWidth = ctx.width;
|
||||||
|
|
||||||
|
// We want to change images if the user taps in the leftmost or
|
||||||
|
// rightmost quarter of the screen
|
||||||
|
bool tappedLeftSide = tapX < screenWidth / 4;
|
||||||
|
bool tappedRightSide = tapX > screenWidth * (3 / 4);
|
||||||
|
|
||||||
|
int? currentPage = controller.page?.toInt();
|
||||||
|
int maxPage = renderList.totalAssets - 1;
|
||||||
|
|
||||||
|
if (tappedLeftSide && currentPage != null) {
|
||||||
|
// Nested if because we don't want to fallback to show/hide controls
|
||||||
|
if (currentPage != 0) {
|
||||||
|
controller.jumpToPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
} else if (tappedRightSide && currentPage != null) {
|
||||||
|
// Nested if because we don't want to fallback to show/hide controls
|
||||||
|
if (currentPage != maxPage) {
|
||||||
|
controller.jumpToPage(currentPage + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ref.read(showControlsProvider.notifier).toggle();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongPressStart: asset.isMotionPhoto
|
onLongPressStart: asset.isMotionPhoto
|
||||||
? (_, __, ___) {
|
? (_, __, ___) {
|
||||||
|
|||||||
@@ -109,10 +109,44 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
if (context.router.current.name == SplashScreenRoute.name) {
|
if (context.router.current.name == SplashScreenRoute.name) {
|
||||||
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
|
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
|
||||||
if (needBetaMigration) {
|
if (needBetaMigration) {
|
||||||
|
bool migrate =
|
||||||
|
(await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text("New Timeline Experience"),
|
||||||
|
content: const Text(
|
||||||
|
"The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?",
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")),
|
||||||
|
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)) ??
|
||||||
|
false;
|
||||||
|
if (migrate != true) {
|
||||||
|
migrate =
|
||||||
|
(await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text("Are you sure?"),
|
||||||
|
content: const Text(
|
||||||
|
"If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?",
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")),
|
||||||
|
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
await Store.put(StoreKey.needBetaMigration, false);
|
await Store.put(StoreKey.needBetaMigration, false);
|
||||||
|
if (migrate) {
|
||||||
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
|
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
|
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -12,6 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_converter.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
@@ -30,27 +29,10 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
|
|
||||||
const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
||||||
Future<Uint8List> _imageToUint8List(Image image) async {
|
|
||||||
final Completer<Uint8List> completer = Completer();
|
|
||||||
image.image
|
|
||||||
.resolve(const ImageConfiguration())
|
|
||||||
.addListener(
|
|
||||||
ImageStreamListener((ImageInfo info, bool _) {
|
|
||||||
info.image.toByteData(format: ImageByteFormat.png).then((byteData) {
|
|
||||||
if (byteData != null) {
|
|
||||||
completer.complete(byteData.buffer.asUint8List());
|
|
||||||
} else {
|
|
||||||
completer.completeError('Failed to convert image to bytes');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, onError: (exception, stackTrace) => completer.completeError(exception)),
|
|
||||||
);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async {
|
Future<void> _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async {
|
||||||
try {
|
try {
|
||||||
final Uint8List imageData = await _imageToUint8List(image);
|
final Uint8List imageData = await imageToUint8List(image);
|
||||||
await ref
|
await ref
|
||||||
.read(fileMediaRepositoryProvider)
|
.read(fileMediaRepositoryProvider)
|
||||||
.saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg");
|
.saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg");
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class LibraryPage extends ConsumerWidget {
|
class LibraryPage extends ConsumerWidget {
|
||||||
@@ -325,7 +325,7 @@ class PlacesCollectionCard extends StatelessWidget {
|
|||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: const Geographic(lat: 21.44950, lon: -157.91959),
|
centre: const LatLng(21.44950, -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class PlacesCollectionPage extends HookConsumerWidget {
|
class PlacesCollectionPage extends HookConsumerWidget {
|
||||||
const PlacesCollectionPage({super.key, this.currentLocation});
|
const PlacesCollectionPage({super.key, this.currentLocation});
|
||||||
final Geographic? currentLocation;
|
final LatLng? currentLocation;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final places = ref.watch(getAllPlacesProvider);
|
final places = ref.watch(getAllPlacesProvider);
|
||||||
@@ -61,7 +61,7 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
|||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)),
|
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)),
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959),
|
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
|
||||||
final descriptionFocusNode = useFocusNode();
|
final descriptionFocusNode = useFocusNode();
|
||||||
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
|
||||||
|
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
|
||||||
|
final slugFocusNode = useFocusNode();
|
||||||
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
final showMetadata = useState(existingLink?.showMetadata ?? true);
|
||||||
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
final allowDownload = useState(existingLink?.allowDownload ?? true);
|
||||||
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
final allowUpload = useState(existingLink?.allowUpload ?? false);
|
||||||
@@ -108,6 +110,26 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildSlugField() {
|
||||||
|
return TextField(
|
||||||
|
controller: slugController,
|
||||||
|
enabled: newShareLink.value.isEmpty,
|
||||||
|
focusNode: slugFocusNode,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'custom_url'.tr(),
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'custom_url'.tr(),
|
||||||
|
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
|
||||||
|
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => slugFocusNode.unfocus(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildShowMetaButton() {
|
Widget buildShowMetaButton() {
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
value: showMetadata.value,
|
value: showMetadata.value,
|
||||||
@@ -261,6 +283,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
allowUpload: allowUpload.value,
|
allowUpload: allowUpload.value,
|
||||||
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
description: descriptionController.text.isEmpty ? null : descriptionController.text,
|
||||||
password: passwordController.text.isEmpty ? null : passwordController.text,
|
password: passwordController.text.isEmpty ? null : passwordController.text,
|
||||||
|
slug: slugController.text.isEmpty ? null : slugController.text,
|
||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||||
);
|
);
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
@@ -274,7 +297,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newLink != null && serverUrl != null) {
|
if (newLink != null && serverUrl != null) {
|
||||||
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
final hasSlug = newLink.slug?.isNotEmpty == true;
|
||||||
|
final urlPath = hasSlug ? newLink.slug : newLink.key;
|
||||||
|
final basePath = hasSlug ? 's' : 'share';
|
||||||
|
newShareLink.value = "$serverUrl$basePath/$urlPath";
|
||||||
copyLinkToClipboard();
|
copyLinkToClipboard();
|
||||||
} else if (newLink == null) {
|
} else if (newLink == null) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -292,6 +318,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
bool? meta;
|
bool? meta;
|
||||||
String? desc;
|
String? desc;
|
||||||
String? password;
|
String? password;
|
||||||
|
String? slug;
|
||||||
DateTime? expiry;
|
DateTime? expiry;
|
||||||
bool? changeExpiry;
|
bool? changeExpiry;
|
||||||
|
|
||||||
@@ -315,6 +342,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
password = passwordController.text;
|
password = passwordController.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (slugController.text != (existingLink!.slug ?? "")) {
|
||||||
|
slug = slugController.text.isEmpty ? null : slugController.text;
|
||||||
|
} else {
|
||||||
|
slug = existingLink!.slug;
|
||||||
|
}
|
||||||
|
|
||||||
if (editExpiry.value) {
|
if (editExpiry.value) {
|
||||||
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
expiry = expiryAfter.value == 0 ? null : calculateExpiry();
|
||||||
changeExpiry = true;
|
changeExpiry = true;
|
||||||
@@ -329,6 +362,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
allowUpload: upload,
|
allowUpload: upload,
|
||||||
description: desc,
|
description: desc,
|
||||||
password: password,
|
password: password,
|
||||||
|
slug: slug,
|
||||||
expiresAt: expiry,
|
expiresAt: expiry,
|
||||||
changeExpiry: changeExpiry,
|
changeExpiry: changeExpiry,
|
||||||
);
|
);
|
||||||
@@ -349,6 +383,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
|
||||||
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
|
||||||
|
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
||||||
child: buildShowMetaButton(),
|
child: buildShowMetaButton(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@@ -11,9 +12,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/models/map/map_event.model.dart' as app;
|
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||||
@@ -26,25 +26,25 @@ import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
|||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
import 'package:immich_mobile/utils/map_utils.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/map/asset_marker_icon.dart';
|
|
||||||
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MapPage extends HookConsumerWidget {
|
class MapPage extends HookConsumerWidget {
|
||||||
const MapPage({super.key, this.initialLocation});
|
const MapPage({super.key, this.initialLocation});
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final mapController = useRef<MapController?>(null);
|
final mapController = useRef<MapLibreMapController?>(null);
|
||||||
final markers = useRef<List<MapMarker>>([]);
|
final markers = useRef<List<MapMarker>>([]);
|
||||||
final markersInBounds = useRef<List<MapMarker>>([]);
|
final markersInBounds = useRef<List<MapMarker>>([]);
|
||||||
final bottomSheetStreamController = useStreamController<app.MapEvent>();
|
final bottomSheetStreamController = useStreamController<MapEvent>();
|
||||||
final selectedMarker = useValueNotifier<MapMarker?>(null);
|
final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null);
|
||||||
final assetsDebouncer = useDebouncer();
|
final assetsDebouncer = useDebouncer();
|
||||||
final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1));
|
final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1));
|
||||||
final isLoading = useProcessingOverlay();
|
final isLoading = useProcessingOverlay();
|
||||||
@@ -55,17 +55,19 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// updates the markersInBounds value with the map markers that are visible in the current
|
// updates the markersInBounds value with the map markers that are visible in the current
|
||||||
// map camera bounds
|
// map camera bounds
|
||||||
void updateAssetsInBounds() {
|
Future<void> updateAssetsInBounds() async {
|
||||||
if (mapController.value == null) return;
|
// Guard map not created
|
||||||
|
if (mapController.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final bounds = mapController.value!.getVisibleRegion();
|
final bounds = await mapController.value!.getVisibleRegion();
|
||||||
final inBounds = markers.value
|
final inBounds = markers.value
|
||||||
.where((m) => bounds.contains(Geographic(lat: m.latLng.lat, lon: m.latLng.lon)))
|
.where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Notify bottom sheet to update asset grid only when there are new assets
|
// Notify bottom sheet to update asset grid only when there are new assets
|
||||||
if (markersInBounds.value.length != inBounds.length) {
|
if (markersInBounds.value.length != inBounds.length) {
|
||||||
bottomSheetStreamController.add(app.MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList()));
|
bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList()));
|
||||||
}
|
}
|
||||||
markersInBounds.value = inBounds;
|
markersInBounds.value = inBounds;
|
||||||
}
|
}
|
||||||
@@ -97,8 +99,7 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Refetch markers when map state is changed
|
// Refetch markers when map state is changed
|
||||||
ref.listen(mapStateNotifierProvider, (_, current) {
|
ref.listen(mapStateNotifierProvider, (_, current) {
|
||||||
if (!current.shouldRefetchMarkers) return;
|
if (current.shouldRefetchMarkers) {
|
||||||
|
|
||||||
markerDebouncer.run(() {
|
markerDebouncer.run(() {
|
||||||
ref.invalidate(mapMarkersProvider);
|
ref.invalidate(mapMarkersProvider);
|
||||||
// Reset marker
|
// Reset marker
|
||||||
@@ -106,58 +107,49 @@ class MapPage extends HookConsumerWidget {
|
|||||||
loadMarkers();
|
loadMarkers();
|
||||||
ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
|
ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
void selectMarker(MapMarker marker) {
|
// updates the selected markers position based on the current map camera
|
||||||
selectedMarker.value = marker;
|
Future<void> updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async {
|
||||||
|
final assetPoint = await mapController.value!.toScreenLocation(marker.latLng);
|
||||||
|
selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate);
|
||||||
|
(assetPoint, marker, shouldAnimate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// finds the nearest asset marker from the tap point and store it as the selectedMarker
|
// finds the nearest asset marker from the tap point and store it as the selectedMarker
|
||||||
void onMarkerClicked(Offset point) {
|
Future<void> onMarkerClicked(Point<double> point, LatLng _) async {
|
||||||
if (mapController.value == null) return;
|
// Guard map not created
|
||||||
|
if (mapController.value == null) {
|
||||||
final features = mapController.value!.featuresInRect(
|
return;
|
||||||
Rect.fromCircle(center: point, radius: 50),
|
}
|
||||||
layerIds: [MapUtils.defaultHeatMapLayerId],
|
final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50);
|
||||||
|
final marker = markersInBounds.value.firstWhereOrNull(
|
||||||
|
(m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)),
|
||||||
);
|
);
|
||||||
|
|
||||||
final featureId = features.firstOrNull?.id?.toString();
|
|
||||||
|
|
||||||
final marker = featureId != null
|
|
||||||
? markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == featureId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
selectMarker(marker);
|
await updateAssetMarkerPosition(marker);
|
||||||
return;
|
} else {
|
||||||
}
|
// If no asset was previously selected and no new asset is available, close the bottom sheet
|
||||||
|
|
||||||
if (selectedMarker.value == null) {
|
if (selectedMarker.value == null) {
|
||||||
// If no asset was previously selected and no new asset is available,
|
bottomSheetStreamController.add(const MapCloseBottomSheet());
|
||||||
// close the bottom sheet.
|
|
||||||
bottomSheetStreamController.add(const app.MapCloseBottomSheet());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedMarker.value = null;
|
selectedMarker.value = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onMapCreated(MapController controller) {
|
void onMapCreated(MapLibreMapController controller) async {
|
||||||
mapController.value = controller;
|
mapController.value = controller;
|
||||||
|
controller.addListener(() {
|
||||||
|
if (controller.isCameraMoving && selectedMarker.value != null) {
|
||||||
|
updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
void onMapEvent(MapEvent event) {
|
|
||||||
switch (event) {
|
|
||||||
case MapEventClick():
|
|
||||||
onMarkerClicked(event.screenPoint);
|
|
||||||
case MapEventCameraIdle():
|
|
||||||
assetsDebouncer.run(updateAssetsInBounds);
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onMarkerTapped() async {
|
Future<void> onMarkerTapped() async {
|
||||||
final assetId = selectedMarker.value?.assetRemoteId;
|
final assetId = selectedMarker.value?.marker.assetRemoteId;
|
||||||
if (assetId == null) {
|
if (assetId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,10 +171,14 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// BOTTOM SHEET CALLBACKS
|
/// BOTTOM SHEET CALLBACKS
|
||||||
|
|
||||||
|
Future<void> onMapMoved() async {
|
||||||
|
assetsDebouncer.run(updateAssetsInBounds);
|
||||||
|
}
|
||||||
|
|
||||||
void onBottomSheetScrolled(String assetRemoteId) {
|
void onBottomSheetScrolled(String assetRemoteId) {
|
||||||
final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
|
final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
|
||||||
if (assetMarker != null) {
|
if (assetMarker != null) {
|
||||||
selectMarker(assetMarker);
|
updateAssetMarkerPosition(assetMarker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +187,10 @@ class MapPage extends HookConsumerWidget {
|
|||||||
if (mapController.value != null && assetMarker != null) {
|
if (mapController.value != null && assetMarker != null) {
|
||||||
// Offset the latitude a little to show the marker just above the viewports center
|
// Offset the latitude a little to show the marker just above the viewports center
|
||||||
final offset = context.isMobile ? 0.02 : 0;
|
final offset = context.isMobile ? 0.02 : 0;
|
||||||
final latlng = Geographic(lat: assetMarker.latLng.lat - offset, lon: assetMarker.latLng.lon);
|
final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude);
|
||||||
mapController.value!.animateCamera(
|
mapController.value!.animateCamera(
|
||||||
center: latlng,
|
CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel),
|
||||||
zoom: mapZoomToAssetLevel,
|
duration: const Duration(milliseconds: 800),
|
||||||
nativeDuration: Durations.extralong2,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,9 +211,8 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (mapController.value != null && location != null) {
|
if (mapController.value != null && location != null) {
|
||||||
await mapController.value!.animateCamera(
|
await mapController.value!.animateCamera(
|
||||||
center: Geographic(lat: location.latitude, lon: location.longitude),
|
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel),
|
||||||
zoom: mapZoomToAssetLevel,
|
duration: const Duration(milliseconds: 800),
|
||||||
nativeDuration: Durations.extralong2,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,8 +234,9 @@ class MapPage extends HookConsumerWidget {
|
|||||||
style: style,
|
style: style,
|
||||||
selectedMarker: selectedMarker,
|
selectedMarker: selectedMarker,
|
||||||
onMapCreated: onMapCreated,
|
onMapCreated: onMapCreated,
|
||||||
onMapEvent: onMapEvent,
|
onMapMoved: onMapMoved,
|
||||||
onStyleLoaded: (_) => reloadLayers(),
|
onMapClicked: onMarkerClicked,
|
||||||
|
onStyleLoaded: reloadLayers,
|
||||||
onMarkerTapped: onMarkerTapped,
|
onMarkerTapped: onMarkerTapped,
|
||||||
),
|
),
|
||||||
// Should be a part of the body and not scaffold::bottomsheet for the
|
// Should be a part of the body and not scaffold::bottomsheet for the
|
||||||
@@ -271,8 +266,9 @@ class MapPage extends HookConsumerWidget {
|
|||||||
style: style,
|
style: style,
|
||||||
selectedMarker: selectedMarker,
|
selectedMarker: selectedMarker,
|
||||||
onMapCreated: onMapCreated,
|
onMapCreated: onMapCreated,
|
||||||
onMapEvent: onMapEvent,
|
onMapMoved: onMapMoved,
|
||||||
onStyleLoaded: (_) => reloadLayers(),
|
onMapClicked: onMarkerClicked,
|
||||||
|
onStyleLoaded: reloadLayers,
|
||||||
onMarkerTapped: onMarkerTapped,
|
onMarkerTapped: onMarkerTapped,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -306,19 +302,32 @@ class MapPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AssetMarkerMeta {
|
||||||
|
final Point<num> point;
|
||||||
|
final MapMarker marker;
|
||||||
|
final bool shouldAnimate;
|
||||||
|
|
||||||
|
const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)';
|
||||||
|
}
|
||||||
|
|
||||||
class _MapWithMarker extends StatelessWidget {
|
class _MapWithMarker extends StatelessWidget {
|
||||||
final AsyncValue<String> style;
|
final AsyncValue<String> style;
|
||||||
final void Function(MapController) onMapCreated;
|
final MapCreatedCallback onMapCreated;
|
||||||
final void Function(MapEvent) onMapEvent;
|
final OnCameraIdleCallback onMapMoved;
|
||||||
final void Function(StyleController) onStyleLoaded;
|
final OnMapClickCallback onMapClicked;
|
||||||
|
final OnStyleLoadedCallback onStyleLoaded;
|
||||||
final Function()? onMarkerTapped;
|
final Function()? onMarkerTapped;
|
||||||
final ValueNotifier<MapMarker?> selectedMarker;
|
final ValueNotifier<_AssetMarkerMeta?> selectedMarker;
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
const _MapWithMarker({
|
const _MapWithMarker({
|
||||||
required this.style,
|
required this.style,
|
||||||
required this.onMapCreated,
|
required this.onMapCreated,
|
||||||
required this.onMapEvent,
|
required this.onMapMoved,
|
||||||
|
required this.onMapClicked,
|
||||||
required this.onStyleLoaded,
|
required this.onStyleLoaded,
|
||||||
required this.selectedMarker,
|
required this.selectedMarker,
|
||||||
this.onMarkerTapped,
|
this.onMarkerTapped,
|
||||||
@@ -327,45 +336,49 @@ class _MapWithMarker extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return style.widgetWhen(
|
return LayoutBuilder(
|
||||||
onData: (style) => MapLibreMap(
|
builder: (ctx, constraints) => SizedBox(
|
||||||
options: MapOptions(
|
height: constraints.maxHeight,
|
||||||
initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0),
|
width: constraints.maxWidth,
|
||||||
initZoom: initialLocation != null ? 12 : 0,
|
child: Stack(
|
||||||
initStyle: style,
|
|
||||||
gestures: const MapGestures.all(pitch: false, rotate: false),
|
|
||||||
),
|
|
||||||
onMapCreated: onMapCreated,
|
|
||||||
onStyleLoaded: onStyleLoaded,
|
|
||||||
onEvent: onMapEvent,
|
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<MapMarker?>(
|
style.widgetWhen(
|
||||||
|
onData: (style) => MapLibreMap(
|
||||||
|
attributionButtonMargins: const Point(8, kToolbarHeight),
|
||||||
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: initialLocation ?? const LatLng(0, 0),
|
||||||
|
zoom: initialLocation != null ? 12 : 0,
|
||||||
|
),
|
||||||
|
styleString: style,
|
||||||
|
// This is needed to update the selectedMarker's position on map camera updates
|
||||||
|
// The changes are notified through the mapController ValueListener which is added in [onMapCreated]
|
||||||
|
trackCameraPosition: true,
|
||||||
|
onMapCreated: onMapCreated,
|
||||||
|
onCameraIdle: onMapMoved,
|
||||||
|
onMapClick: onMapClicked,
|
||||||
|
onStyleLoadedCallback: onStyleLoaded,
|
||||||
|
tiltGesturesEnabled: false,
|
||||||
|
dragEnabled: false,
|
||||||
|
myLocationEnabled: false,
|
||||||
|
attributionButtonPosition: AttributionButtonPosition.topRight,
|
||||||
|
rotateGesturesEnabled: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ValueListenableBuilder(
|
||||||
valueListenable: selectedMarker,
|
valueListenable: selectedMarker,
|
||||||
builder: (ctx, marker, _) => marker != null
|
builder: (ctx, value, _) => value != null
|
||||||
? WidgetLayer(
|
? PositionedAssetMarkerIcon(
|
||||||
markers: [
|
point: value.point,
|
||||||
Marker(
|
assetRemoteId: value.marker.assetRemoteId,
|
||||||
point: marker.latLng,
|
assetThumbhash: '',
|
||||||
size: const Size(100, 100),
|
durationInMilliseconds: value.shouldAnimate ? 100 : 0,
|
||||||
alignment: Alignment.bottomCenter,
|
onTap: onMarkerTapped,
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => onMarkerTapped?.call(),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 100,
|
|
||||||
child: AssetMarkerIcon(
|
|
||||||
id: marker.assetRemoteId,
|
|
||||||
thumbhash: '',
|
|
||||||
key: Key(marker.assetRemoteId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -5,34 +7,36 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
import 'package:immich_mobile/utils/map_utils.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MapLocationPickerPage extends HookConsumerWidget {
|
class MapLocationPickerPage extends HookConsumerWidget {
|
||||||
final Geographic initialLatLng;
|
final LatLng initialLatLng;
|
||||||
|
|
||||||
const MapLocationPickerPage({super.key, this.initialLatLng = const Geographic(lat: 0, lon: 0)});
|
const MapLocationPickerPage({super.key, this.initialLatLng = const LatLng(0, 0)});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedLatLng = useValueNotifier<Geographic>(initialLatLng);
|
final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
|
||||||
final currentLatLng = useValueListenable(selectedLatLng);
|
final controller = useRef<MapLibreMapController?>(null);
|
||||||
final controller = useRef<MapController?>(null);
|
final marker = useRef<Symbol?>(null);
|
||||||
|
|
||||||
Future<void> onStyleLoaded(StyleController style) async {
|
Future<void> onStyleLoaded() async {
|
||||||
await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png');
|
marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEvent(MapEvent event) {
|
Future<void> onMapClick(Point<num> _, LatLng centre) async {
|
||||||
if (event is! MapEventClick) return;
|
selectedLatLng.value = centre;
|
||||||
|
await controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
|
||||||
selectedLatLng.value = event.point;
|
if (marker.value != null) {
|
||||||
controller.value?.animateCamera(center: event.point);
|
await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onClose([Geographic? selected]) {
|
void onClose([LatLng? selected]) {
|
||||||
context.maybePop(selected);
|
context.maybePop(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +47,9 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentLatLng = Geographic(lat: currentLocation.latitude, lon: currentLocation.longitude);
|
var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
|
||||||
selectedLatLng.value = currentLatLng;
|
selectedLatLng.value = currentLatLng;
|
||||||
await controller.value?.animateCamera(center: currentLatLng, zoom: 12);
|
await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12));
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
@@ -62,24 +66,18 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
||||||
),
|
),
|
||||||
child: MapLibreMap(
|
child: MapLibreMap(
|
||||||
options: MapOptions(
|
initialCameraPosition: CameraPosition(
|
||||||
initCenter: initialLatLng,
|
target: initialLatLng,
|
||||||
initZoom: (initialLatLng.lat == 0 && initialLatLng.lon == 0) ? 1 : 12,
|
zoom: (initialLatLng.latitude == 0 && initialLatLng.longitude == 0) ? 1 : 12,
|
||||||
initStyle: style,
|
|
||||||
gestures: const MapGestures.all(pitch: false),
|
|
||||||
),
|
),
|
||||||
|
styleString: style,
|
||||||
onMapCreated: (mapController) => controller.value = mapController,
|
onMapCreated: (mapController) => controller.value = mapController,
|
||||||
onStyleLoaded: onStyleLoaded,
|
onStyleLoadedCallback: onStyleLoaded,
|
||||||
onEvent: onEvent,
|
onMapClick: onMapClick,
|
||||||
layers: [
|
dragEnabled: false,
|
||||||
MarkerLayer(
|
tiltGesturesEnabled: false,
|
||||||
points: [Feature(geometry: Point(currentLatLng))],
|
myLocationEnabled: false,
|
||||||
iconImage: 'mapMarker',
|
attributionButtonMargins: const Point(20, 15),
|
||||||
iconSize: 0.15,
|
|
||||||
iconAnchor: IconAnchor.bottom,
|
|
||||||
iconAllowOverlap: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -119,7 +117,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BottomBar extends StatelessWidget {
|
class _BottomBar extends StatelessWidget {
|
||||||
final ValueNotifier<Geographic> selectedLatLng;
|
final ValueNotifier<LatLng> selectedLatLng;
|
||||||
final Function() onUseLocation;
|
final Function() onUseLocation;
|
||||||
final Function() onGetCurrentLocation;
|
final Function() onGetCurrentLocation;
|
||||||
|
|
||||||
@@ -142,7 +140,8 @@ class _BottomBar extends StatelessWidget {
|
|||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: selectedLatLng,
|
valueListenable: selectedLatLng,
|
||||||
builder: (_, value, __) => Text("${value.lat.toStringAsFixed(4)}, ${value.lon.toStringAsFixed(4)}"),
|
builder: (_, value, __) =>
|
||||||
|
Text("${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -90,14 +90,11 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadMoreSearchResult() async {
|
loadMoreSearchResult() async {
|
||||||
isSearching.value = true;
|
|
||||||
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
||||||
|
|
||||||
if (!hasResult) {
|
if (!hasResult) {
|
||||||
context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr()));
|
context.showSnackBar(searchInfoSnackBar('search_no_more_result'.tr()));
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearching.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPrefilter() {
|
searchPrefilter() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftLibraryPage extends ConsumerWidget {
|
class DriftLibraryPage extends ConsumerWidget {
|
||||||
@@ -230,7 +230,7 @@ class _PlacesCollectionCard extends StatelessWidget {
|
|||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: const Geographic(lat: 21.44950, lon: -157.91959),
|
centre: const LatLng(21.44950, -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftMapPage extends StatelessWidget {
|
class DriftMapPage extends StatelessWidget {
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
const DriftMapPage({super.key, this.initialLocation});
|
const DriftMapPage({super.key, this.initialLocation});
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftPlacePage extends StatelessWidget {
|
class DriftPlacePage extends StatelessWidget {
|
||||||
const DriftPlacePage({super.key, this.currentLocation});
|
const DriftPlacePage({super.key, this.currentLocation});
|
||||||
|
|
||||||
final Geographic? currentLocation;
|
final LatLng? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -82,7 +82,7 @@ class _Map extends StatelessWidget {
|
|||||||
const _Map({required this.search, this.currentLocation});
|
const _Map({required this.search, this.currentLocation});
|
||||||
|
|
||||||
final ValueNotifier<String?> search;
|
final ValueNotifier<String?> search;
|
||||||
final Geographic? currentLocation;
|
final LatLng? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -96,7 +96,7 @@ class _Map extends StatelessWidget {
|
|||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
onTap: (_, __) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)),
|
onTap: (_, __) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)),
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959),
|
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
@@ -14,6 +13,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_converter.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -33,23 +33,6 @@ class DriftEditImagePage extends ConsumerWidget {
|
|||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
|
|
||||||
const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited});
|
||||||
Future<Uint8List> _imageToUint8List(Image image) async {
|
|
||||||
final Completer<Uint8List> completer = Completer();
|
|
||||||
image.image
|
|
||||||
.resolve(const ImageConfiguration())
|
|
||||||
.addListener(
|
|
||||||
ImageStreamListener((ImageInfo info, bool _) {
|
|
||||||
info.image.toByteData(format: ImageByteFormat.png).then((byteData) {
|
|
||||||
if (byteData != null) {
|
|
||||||
completer.complete(byteData.buffer.asUint8List());
|
|
||||||
} else {
|
|
||||||
completer.completeError('Failed to convert image to bytes');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, onError: (exception, stackTrace) => completer.completeError(exception)),
|
|
||||||
);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exitEditing(BuildContext context) {
|
void _exitEditing(BuildContext context) {
|
||||||
// this assumes that the only way to get to this page is from the AssetViewerRoute
|
// this assumes that the only way to get to this page is from the AssetViewerRoute
|
||||||
@@ -58,7 +41,7 @@ class DriftEditImagePage extends ConsumerWidget {
|
|||||||
|
|
||||||
Future<void> _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async {
|
Future<void> _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async {
|
||||||
try {
|
try {
|
||||||
final Uint8List imageData = await _imageToUint8List(image);
|
final Uint8List imageData = await imageToUint8List(image);
|
||||||
LocalAsset? localAsset;
|
LocalAsset? localAsset;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:crop_image/crop_image.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_converter.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class ProfilePictureCropPage extends ConsumerStatefulWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
|
||||||
|
const ProfilePictureCropPage({super.key, required this.asset});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ProfilePictureCropPage> createState() => _ProfilePictureCropPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfilePictureCropPageState extends ConsumerState<ProfilePictureCropPage> {
|
||||||
|
late final CropController _cropController;
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _didInitCropController = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_cropController = CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1));
|
||||||
|
|
||||||
|
// Lock aspect ratio to 1:1 for circular/square crop
|
||||||
|
// CropController depends on CropImage initializing its bitmap size.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted || _didInitCropController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_didInitCropController = true;
|
||||||
|
|
||||||
|
_cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9);
|
||||||
|
_cropController.aspectRatio = 1.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_cropController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDone() async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final croppedImage = await _cropController.croppedImage();
|
||||||
|
final pngBytes = await imageToUint8List(croppedImage);
|
||||||
|
final xFile = XFile.fromData(pngBytes, mimeType: 'image/png');
|
||||||
|
final success = await ref
|
||||||
|
.read(uploadProfileImageProvider.notifier)
|
||||||
|
.upload(xFile, fileName: 'profile-picture.png');
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
|
||||||
|
ref.read(authProvider.notifier).updateUserProfileImagePath(profileImagePath);
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user != null) {
|
||||||
|
unawaited(ref.read(currentUserProvider.notifier).refresh());
|
||||||
|
}
|
||||||
|
unawaited(ref.read(backupProvider.notifier).updateDiskInfo());
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'profile_picture_set'.tr(),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
unawaited(context.maybePop());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'errors.unable_to_set_profile_picture'.tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'errors.unable_to_set_profile_picture'.tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Create Image widget from asset
|
||||||
|
final image = Image(image: getFullImageProvider(widget.asset));
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
title: Text("set_profile_picture".tr()),
|
||||||
|
leading: _isLoading ? null : const ImmichCloseButton(),
|
||||||
|
actions: [
|
||||||
|
if (_isLoading)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ImmichIconButton(
|
||||||
|
icon: Icons.done_rounded,
|
||||||
|
color: ImmichColor.primary,
|
||||||
|
variant: ImmichVariant.ghost,
|
||||||
|
onPressed: _handleDone,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
|
spreadRadius: 2,
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
child: CropImage(controller: _cropController, image: image, gridColor: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
@@ -116,15 +115,17 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
search() => searchFilter(filter.value);
|
search() => searchFilter(filter.value);
|
||||||
|
|
||||||
|
final isLoadingMore = useState(false);
|
||||||
|
|
||||||
loadMoreSearchResult() async {
|
loadMoreSearchResult() async {
|
||||||
isSearching.value = true;
|
if (isLoadingMore.value) return;
|
||||||
|
isLoadingMore.value = true;
|
||||||
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
||||||
|
isLoadingMore.value = false;
|
||||||
|
|
||||||
if (!hasResult) {
|
if (!hasResult) {
|
||||||
context.showSnackBar(searchInfoSnackBar('search_no_more_result'.t(context: context)));
|
context.showSnackBar(searchInfoSnackBar('search_no_more_result'.t(context: context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearching.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPreFilter() {
|
searchPreFilter() {
|
||||||
@@ -745,7 +746,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
if (isSearching.value)
|
if (isSearching.value)
|
||||||
const SliverFillRemaining(hasScrollBody: false, child: Center(child: CircularProgressIndicator()))
|
const SliverFillRemaining(hasScrollBody: false, child: Center(child: CircularProgressIndicator()))
|
||||||
else
|
else
|
||||||
_SearchResultGrid(onScrollEnd: loadMoreSearchResult),
|
_SearchResultGrid(onScrollEnd: loadMoreSearchResult, isLoadingMore: isLoadingMore.value),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -754,28 +755,30 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
class _SearchResultGrid extends ConsumerWidget {
|
class _SearchResultGrid extends ConsumerWidget {
|
||||||
final VoidCallback onScrollEnd;
|
final VoidCallback onScrollEnd;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
|
||||||
const _SearchResultGrid({required this.onScrollEnd});
|
const _SearchResultGrid({required this.onScrollEnd, this.isLoadingMore = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final assets = ref.watch(paginatedSearchProvider.select((s) => s.assets));
|
final hasAssets = ref.watch(paginatedSearchProvider.select((s) => s.assets.isNotEmpty));
|
||||||
|
|
||||||
if (assets.isEmpty) {
|
if (!hasAssets) {
|
||||||
return const _SearchEmptyContent();
|
return const _SearchEmptyContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return NotificationListener<ScrollEndNotification>(
|
return NotificationListener<ScrollUpdateNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
final isBottomSheetNotification =
|
final isBottomSheetNotification =
|
||||||
notification.context?.findAncestorWidgetOfExactType<DraggableScrollableSheet>() != null;
|
notification.context?.findAncestorWidgetOfExactType<DraggableScrollableSheet>() != null;
|
||||||
|
|
||||||
final metrics = notification.metrics;
|
final metrics = notification.metrics;
|
||||||
final isVerticalScroll = metrics.axis == Axis.vertical;
|
final isVerticalScroll = metrics.axis == Axis.vertical;
|
||||||
|
final remaining = metrics.maxScrollExtent - metrics.pixels;
|
||||||
|
|
||||||
if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) {
|
if (remaining < metrics.viewportDimension && isVerticalScroll && !isBottomSheetNotification) {
|
||||||
onScrollEnd();
|
onScrollEnd();
|
||||||
ref.read(paginatedSearchProvider.notifier).setScrollOffset(metrics.maxScrollExtent);
|
ref.read(paginatedSearchProvider.notifier).setScrollOffset(metrics.pixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -784,18 +787,29 @@ class _SearchResultGrid extends ConsumerWidget {
|
|||||||
child: ProviderScope(
|
child: ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets, TimelineOrigin.search);
|
return ref.watch(paginatedSearchTimelineProvider);
|
||||||
ref.onDispose(timelineService.dispose);
|
|
||||||
return timelineService;
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: Timeline(
|
||||||
key: ValueKey(assets.length),
|
|
||||||
groupBy: GroupAssetsBy.none,
|
groupBy: GroupAssetsBy.none,
|
||||||
appBar: null,
|
appBar: null,
|
||||||
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||||
snapToMonth: false,
|
snapToMonth: false,
|
||||||
initialScrollOffset: ref.read(paginatedSearchProvider.select((s) => s.scrollOffset)),
|
initialScrollOffset: ref.read(paginatedSearchProvider.select((s) => s.scrollOffset)),
|
||||||
|
bottomSliverWidget: isLoadingMore
|
||||||
|
? const SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32),
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/search_result.model.dart';
|
import 'package:immich_mobile/domain/models/search_result.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/search.service.dart';
|
import 'package:immich_mobile/domain/services/search.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
|
||||||
|
|
||||||
@@ -58,3 +63,49 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
|||||||
state = const SearchResult(assets: [], nextPage: 1, scrollOffset: 0.0);
|
state = const SearchResult(assets: [], nextPage: 1, scrollOffset: 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [TimelineService] for search results that stays alive across pagination.
|
||||||
|
/// Instead of recreating the service on each page load, it uses a long-lived
|
||||||
|
/// bucket stream that emits new buckets whenever the search assets change.
|
||||||
|
final paginatedSearchTimelineProvider = Provider.autoDispose<TimelineService>((ref) {
|
||||||
|
final controller = StreamController<List<Bucket>>.broadcast();
|
||||||
|
|
||||||
|
List<Bucket> generateBuckets(int count) {
|
||||||
|
if (count == 0) return [];
|
||||||
|
final buckets = List.filled(
|
||||||
|
(count / kTimelineNoneSegmentSize).ceil(),
|
||||||
|
const Bucket(assetCount: kTimelineNoneSegmentSize),
|
||||||
|
);
|
||||||
|
if (count % kTimelineNoneSegmentSize != 0) {
|
||||||
|
buckets[buckets.length - 1] = Bucket(assetCount: count % kTimelineNoneSegmentSize);
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit new buckets whenever asset count changes
|
||||||
|
ref.listen(paginatedSearchProvider.select((s) => s.assets.length), (_, count) {
|
||||||
|
controller.add(generateBuckets(count));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Each subscriber gets the current bucket state then follows updates
|
||||||
|
Stream<List<Bucket>> bucketSource() async* {
|
||||||
|
yield generateBuckets(ref.read(paginatedSearchProvider).assets.length);
|
||||||
|
yield* controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
final service = TimelineService((
|
||||||
|
bucketSource: bucketSource,
|
||||||
|
assetSource: (offset, count) {
|
||||||
|
final assets = ref.read(paginatedSearchProvider).assets;
|
||||||
|
return Future.value(assets.skip(offset).take(count).toList(growable: false));
|
||||||
|
},
|
||||||
|
origin: TimelineOrigin.search,
|
||||||
|
));
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
controller.close();
|
||||||
|
service.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return service;
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/trash_delete_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
/// This delete action has the following behavior:
|
/// This delete action has the following behavior:
|
||||||
@@ -22,6 +23,18 @@ class DeleteTrashActionButton extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
|
||||||
|
|
||||||
|
final confirmDelete =
|
||||||
|
await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => TrashDeleteDialog(count: selectCount),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (!confirmDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
|
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
class SetAlbumCoverActionButton extends ConsumerWidget {
|
||||||
|
final String albumId;
|
||||||
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const SetAlbumCoverActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.albumId,
|
||||||
|
required this.source,
|
||||||
|
this.iconOnly = false,
|
||||||
|
this.menuItem = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'album_cover_updated'.t(context: context);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.image_outlined,
|
||||||
|
label: 'set_as_album_cover'.t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
maxWidth: 100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class SetProfilePictureActionButton extends ConsumerWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
final bool iconOnly;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context) {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pushRoute(ProfilePictureCropRoute(asset: asset));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.account_circle_outlined,
|
||||||
|
label: "set_as_profile_picture".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context),
|
||||||
|
maxWidth: 100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-6
@@ -10,7 +10,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widge
|
|||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class LocationDetails extends ConsumerStatefulWidget {
|
class LocationDetails extends ConsumerStatefulWidget {
|
||||||
const LocationDetails({super.key});
|
const LocationDetails({super.key});
|
||||||
@@ -20,7 +20,7 @@ class LocationDetails extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
||||||
MapController? _mapController;
|
MapLibreMapController? _mapController;
|
||||||
|
|
||||||
String? _getLocationName(ExifInfo? exifInfo) {
|
String? _getLocationName(ExifInfo? exifInfo) {
|
||||||
if (exifInfo == null) {
|
if (exifInfo == null) {
|
||||||
@@ -36,16 +36,14 @@ class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMapCreated(MapController controller) {
|
void _onMapCreated(MapLibreMapController controller) {
|
||||||
_mapController = controller;
|
_mapController = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
|
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
|
||||||
final currentExif = current.valueOrNull;
|
final currentExif = current.valueOrNull;
|
||||||
if (currentExif != null && currentExif.hasCoordinates) {
|
if (currentExif != null && currentExif.hasCoordinates) {
|
||||||
_mapController?.moveCamera(
|
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!)));
|
||||||
center: Geographic(lat: currentExif.latitude!, lon: currentExif.longitude!),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.sta
|
|||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@@ -29,8 +31,9 @@ enum _DragIntent { none, scroll, dismiss }
|
|||||||
class AssetPage extends ConsumerStatefulWidget {
|
class AssetPage extends ConsumerStatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
|
final void Function(int direction)? onTapNavigate;
|
||||||
|
|
||||||
const AssetPage({super.key, required this.index, required this.heroOffset});
|
const AssetPage({super.key, required this.index, required this.heroOffset, this.onTapNavigate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _AssetPageState();
|
ConsumerState createState() => _AssetPageState();
|
||||||
@@ -50,14 +53,13 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController);
|
late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController);
|
||||||
|
final ValueNotifier<PhotoViewScaleState> _videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial);
|
||||||
|
|
||||||
double _snapOffset = 0.0;
|
double _snapOffset = 0.0;
|
||||||
double _lastScrollOffset = 0.0;
|
|
||||||
|
|
||||||
DragStartDetails? _dragStart;
|
DragStartDetails? _dragStart;
|
||||||
_DragIntent _dragIntent = _DragIntent.none;
|
_DragIntent _dragIntent = _DragIntent.none;
|
||||||
Drag? _drag;
|
Drag? _drag;
|
||||||
bool _shouldPopOnDrag = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -78,6 +80,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
_proxyScrollController.dispose();
|
_proxyScrollController.dispose();
|
||||||
_scaleBoundarySub?.cancel();
|
_scaleBoundarySub?.cancel();
|
||||||
_eventSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
|
_videoScaleStateNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +94,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
void _showDetails() {
|
void _showDetails() {
|
||||||
if (!_proxyScrollController.hasClients || _snapOffset <= 0) return;
|
if (!_proxyScrollController.hasClients || _snapOffset <= 0) return;
|
||||||
_lastScrollOffset = _proxyScrollController.offset;
|
|
||||||
_proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
|
_proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,18 +107,15 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
final offset = _proxyScrollController.offset;
|
final offset = _proxyScrollController.offset;
|
||||||
if (offset > SnapScrollPhysics.minSnapDistance && offset > _lastScrollOffset) {
|
if (offset > SnapScrollPhysics.minSnapDistance) {
|
||||||
_viewer.setShowingDetails(true);
|
_viewer.setShowingDetails(true);
|
||||||
} else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) {
|
} else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) {
|
||||||
_viewer.setShowingDetails(false);
|
_viewer.setShowingDetails(false);
|
||||||
}
|
}
|
||||||
_lastScrollOffset = offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _beginDrag(DragStartDetails details) {
|
void _beginDrag(DragStartDetails details) {
|
||||||
_dragStart = details;
|
_dragStart = details;
|
||||||
_shouldPopOnDrag = false;
|
|
||||||
_lastScrollOffset = _proxyScrollController.hasClients ? _proxyScrollController.offset : 0.0;
|
|
||||||
|
|
||||||
if (_viewController != null) {
|
if (_viewController != null) {
|
||||||
_initialPhotoViewState = _viewController!.value;
|
_initialPhotoViewState = _viewController!.value;
|
||||||
@@ -158,6 +157,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
void _endDrag(DragEndDetails details) {
|
void _endDrag(DragEndDetails details) {
|
||||||
if (_dragStart == null) return;
|
if (_dragStart == null) return;
|
||||||
|
|
||||||
|
final start = _dragStart;
|
||||||
_dragStart = null;
|
_dragStart = null;
|
||||||
|
|
||||||
final intent = _dragIntent;
|
final intent = _dragIntent;
|
||||||
@@ -173,7 +173,8 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
_drag?.end(details);
|
_drag?.end(details);
|
||||||
_drag = null;
|
_drag = null;
|
||||||
case _DragIntent.dismiss:
|
case _DragIntent.dismiss:
|
||||||
if (_shouldPopOnDrag) {
|
const popThreshold = 75.0;
|
||||||
|
if (details.localPosition.dy - start!.localPosition.dy > popThreshold) {
|
||||||
context.maybePop();
|
context.maybePop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -192,7 +193,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
PhotoViewControllerBase controller,
|
PhotoViewControllerBase controller,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
) {
|
) {
|
||||||
_viewController = controller;
|
|
||||||
if (!_showingDetails && _isZoomed) return;
|
if (!_showingDetails && _isZoomed) return;
|
||||||
_beginDrag(details);
|
_beginDrag(details);
|
||||||
}
|
}
|
||||||
@@ -206,12 +206,8 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
|
|
||||||
void _handleDragDown(BuildContext context, Offset delta) {
|
void _handleDragDown(BuildContext context, Offset delta) {
|
||||||
const dragRatio = 0.2;
|
const dragRatio = 0.2;
|
||||||
const popThreshold = 75.0;
|
|
||||||
|
|
||||||
_shouldPopOnDrag = delta.dy > popThreshold;
|
|
||||||
|
|
||||||
final distance = delta.dy.abs();
|
final distance = delta.dy.abs();
|
||||||
|
|
||||||
final maxScaleDistance = context.height * 0.5;
|
final maxScaleDistance = context.height * 0.5;
|
||||||
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
||||||
final initialScale = _viewController?.initialScale ?? _initialPhotoViewState.scale;
|
final initialScale = _viewController?.initialScale ?? _initialPhotoViewState.scale;
|
||||||
@@ -224,17 +220,39 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTapUp(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue) {
|
void _onTapUp(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue) {
|
||||||
if (!_showingDetails && _dragStart == null) _viewer.toggleControls();
|
if (_showingDetails || _dragStart != null) return;
|
||||||
|
|
||||||
|
final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.tapToNavigate);
|
||||||
|
if (!tapToNavigate) {
|
||||||
|
_viewer.toggleControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tapX = details.globalPosition.dx;
|
||||||
|
final screenWidth = context.width;
|
||||||
|
|
||||||
|
// Navigate if the user taps in the leftmost or rightmost quarter of the screen
|
||||||
|
final tappedLeftSide = tapX < screenWidth / 4;
|
||||||
|
final tappedRightSide = tapX > screenWidth * (3 / 4);
|
||||||
|
|
||||||
|
if (tappedLeftSide) {
|
||||||
|
widget.onTapNavigate?.call(-1);
|
||||||
|
} else if (tappedRightSide) {
|
||||||
|
widget.onTapNavigate?.call(1);
|
||||||
|
} else {
|
||||||
|
_viewer.toggleControls();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLongPress(BuildContext context, LongPressStartDetails details, PhotoViewControllerValue controllerValue) =>
|
void _onLongPress(BuildContext context, LongPressStartDetails details, PhotoViewControllerValue controllerValue) =>
|
||||||
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
|
||||||
|
|
||||||
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
||||||
_isZoomed = switch (scaleState) {
|
_isZoomed =
|
||||||
PhotoViewScaleState.zoomedIn || PhotoViewScaleState.covering => true,
|
scaleState == PhotoViewScaleState.zoomedIn ||
|
||||||
_ => false,
|
scaleState == PhotoViewScaleState.covering ||
|
||||||
};
|
_videoScaleStateNotifier.value == PhotoViewScaleState.zoomedIn ||
|
||||||
|
_videoScaleStateNotifier.value == PhotoViewScaleState.covering;
|
||||||
_viewer.setZoomed(_isZoomed);
|
_viewer.setZoomed(_isZoomed);
|
||||||
|
|
||||||
if (scaleState != PhotoViewScaleState.initial) {
|
if (scaleState != PhotoViewScaleState.initial) {
|
||||||
@@ -316,36 +334,35 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PhotoView.customChild(
|
return PhotoView.customChild(
|
||||||
|
key: ValueKey(displayAsset),
|
||||||
onDragStart: _onDragStart,
|
onDragStart: _onDragStart,
|
||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
onDragCancel: _onDragCancel,
|
onDragCancel: _onDragCancel,
|
||||||
onTapUp: _onTapUp,
|
|
||||||
heroAttributes: heroAttributes,
|
heroAttributes: heroAttributes,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
maxScale: 1.0,
|
|
||||||
basePosition: Alignment.center,
|
basePosition: Alignment.center,
|
||||||
disableScaleGestures: true,
|
disableScaleGestures: true,
|
||||||
scaleStateChangedCallback: _onScaleStateChanged,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
|
tightMode: true,
|
||||||
onPageBuild: _onPageBuild,
|
onPageBuild: _onPageBuild,
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
child: SizedBox(
|
|
||||||
width: context.width,
|
|
||||||
height: context.height,
|
|
||||||
child: NativeVideoViewer(
|
child: NativeVideoViewer(
|
||||||
key: ValueKey(displayAsset.heroTag),
|
|
||||||
asset: displayAsset,
|
|
||||||
image: Image(
|
|
||||||
key: ValueKey(displayAsset),
|
key: ValueKey(displayAsset),
|
||||||
|
asset: displayAsset,
|
||||||
|
scaleStateNotifier: _videoScaleStateNotifier,
|
||||||
|
disableScaleGestures: showingDetails,
|
||||||
|
image: Image(
|
||||||
|
key: ValueKey(displayAsset.heroTag),
|
||||||
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
image: getFullImageProvider(displayAsset, size: context.sizeData),
|
||||||
fit: BoxFit.contain,
|
|
||||||
height: context.height,
|
height: context.height,
|
||||||
width: context.width,
|
width: context.width,
|
||||||
|
fit: BoxFit.contain,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,16 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
bool _assetReloadRequested = false;
|
bool _assetReloadRequested = false;
|
||||||
|
|
||||||
|
void _onTapNavigate(int direction) {
|
||||||
|
final page = _pageController.page?.toInt();
|
||||||
|
if (page == null) return;
|
||||||
|
final target = page + direction;
|
||||||
|
final maxPage = ref.read(timelineServiceProvider).totalAssets - 1;
|
||||||
|
if (target >= 0 && target <= maxPage) {
|
||||||
|
_pageController.jumpToPage(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -270,7 +280,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
: const FastClampingScrollPhysics(),
|
: const FastClampingScrollPhysics(),
|
||||||
itemCount: ref.read(timelineServiceProvider).totalAssets,
|
itemCount: ref.read(timelineServiceProvider).totalAssets,
|
||||||
onPageChanged: (index) => _onAssetChanged(index),
|
onPageChanged: (index) => _onAssetChanged(index),
|
||||||
itemBuilder: (context, index) => AssetPage(index: index, heroOffset: _heroOffset),
|
itemBuilder: (context, index) =>
|
||||||
|
AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!CurrentPlatform.isIOS)
|
if (!CurrentPlatform.isIOS)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
@@ -25,6 +26,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
import 'package:immich_mobile/utils/hooks/interval_hook.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:native_video_player/native_video_player.dart';
|
import 'package:native_video_player/native_video_player.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -52,6 +54,8 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
final bool showControls;
|
final bool showControls;
|
||||||
final int playbackDelayFactor;
|
final int playbackDelayFactor;
|
||||||
final Widget image;
|
final Widget image;
|
||||||
|
final ValueNotifier<PhotoViewScaleState>? scaleStateNotifier;
|
||||||
|
final bool disableScaleGestures;
|
||||||
|
|
||||||
const NativeVideoViewer({
|
const NativeVideoViewer({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -59,6 +63,8 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
required this.image,
|
required this.image,
|
||||||
this.showControls = true,
|
this.showControls = true,
|
||||||
this.playbackDelayFactor = 1,
|
this.playbackDelayFactor = 1,
|
||||||
|
this.scaleStateNotifier,
|
||||||
|
this.disableScaleGestures = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -138,6 +144,7 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
|
|
||||||
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
|
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
|
||||||
final aspectRatio = useState<double?>(null);
|
final aspectRatio = useState<double?>(null);
|
||||||
|
|
||||||
useMemoized(() async {
|
useMemoized(() async {
|
||||||
if (!context.mounted || aspectRatio.value != null) {
|
if (!context.mounted || aspectRatio.value != null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -313,6 +320,20 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
Timer(const Duration(milliseconds: 200), checkIfBuffering);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Size? videoContextSize(double? videoAspectRatio, BuildContext? context) {
|
||||||
|
Size? videoContextSize;
|
||||||
|
if (videoAspectRatio == null || context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final contextAspectRatio = context.width / context.height;
|
||||||
|
if (videoAspectRatio > contextAspectRatio) {
|
||||||
|
videoContextSize = Size(context.width, context.width / aspectRatio.value!);
|
||||||
|
} else {
|
||||||
|
videoContextSize = Size(context.height * aspectRatio.value!, context.height);
|
||||||
|
}
|
||||||
|
return videoContextSize;
|
||||||
|
}
|
||||||
|
|
||||||
ref.listen(currentAssetNotifier, (_, value) {
|
ref.listen(currentAssetNotifier, (_, value) {
|
||||||
final playerController = controller.value;
|
final playerController = controller.value;
|
||||||
if (playerController != null && value != asset) {
|
if (playerController != null && value != asset) {
|
||||||
@@ -393,26 +414,31 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Stack(
|
return SizedBox(
|
||||||
|
width: context.width,
|
||||||
|
height: context.height,
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// This remains under the video to avoid flickering
|
// Hide thumbnail once video is visible to avoid it showing in background when zooming out on video.
|
||||||
// For motion videos, this is the image portion of the asset
|
if (!isVisible.value || controller.value == null) Center(key: ValueKey(asset.heroTag), child: image),
|
||||||
Center(key: ValueKey(asset.heroTag), child: image),
|
if (aspectRatio.value != null && !isCasting && isCurrent)
|
||||||
if (aspectRatio.value != null && !isCasting)
|
|
||||||
Visibility.maintain(
|
Visibility.maintain(
|
||||||
key: ValueKey(asset),
|
key: ValueKey(asset),
|
||||||
visible: isVisible.value,
|
visible: isVisible.value,
|
||||||
child: Center(
|
child: PhotoView.customChild(
|
||||||
key: ValueKey(asset),
|
key: ValueKey(asset),
|
||||||
child: AspectRatio(
|
enableRotation: false,
|
||||||
key: ValueKey(asset),
|
disableScaleGestures: disableScaleGestures,
|
||||||
aspectRatio: aspectRatio.value!,
|
// Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet.
|
||||||
child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null,
|
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
|
||||||
),
|
scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state,
|
||||||
|
childSize: videoContextSize(aspectRatio.value, context),
|
||||||
|
child: NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showControls) const Center(child: VideoViewerControls()),
|
if (showControls) const Center(child: VideoViewerControls()),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,19 +81,28 @@ class VideoViewerControls extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleControlsVisibility() {
|
||||||
|
if (showBuffering) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showControls) {
|
||||||
|
ref.read(assetViewerProvider.notifier).setControls(false);
|
||||||
|
} else {
|
||||||
|
showControlsAndStartHideTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: showControlsAndStartHideTimer,
|
onTap: toggleControlsVisibility,
|
||||||
child: AbsorbPointer(
|
child: IgnorePointer(
|
||||||
absorbing: !showControls,
|
ignoring: !showControls,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (showBuffering)
|
if (showBuffering)
|
||||||
const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400)))
|
const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400)))
|
||||||
else
|
else
|
||||||
GestureDetector(
|
CenterPlayButton(
|
||||||
onTap: () => ref.read(assetViewerProvider.notifier).setControls(false),
|
|
||||||
child: CenterPlayButton(
|
|
||||||
backgroundColor: Colors.black54,
|
backgroundColor: Colors.black54,
|
||||||
iconColor: Colors.white,
|
iconColor: Colors.white,
|
||||||
isFinished: state == VideoPlaybackState.completed,
|
isFinished: state == VideoPlaybackState.completed,
|
||||||
@@ -102,7 +111,6 @@ class VideoViewerControls extends HookConsumerWidget {
|
|||||||
show: assetIsVideo && showControls,
|
show: assetIsVideo && showControls,
|
||||||
onPressed: togglePlay,
|
onPressed: togglePlay,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
@@ -113,6 +114,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
|||||||
],
|
],
|
||||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
|
if (ownsAlbum && multiselect.selectedAssets.length == 1)
|
||||||
|
SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
],
|
],
|
||||||
slivers: ownsAlbum
|
slivers: ownsAlbum
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class MapState {
|
class MapState {
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
final LngLatBounds bounds;
|
final LatLngBounds bounds;
|
||||||
final bool onlyFavorites;
|
final bool onlyFavorites;
|
||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
@@ -35,7 +35,7 @@ class MapState {
|
|||||||
int get hashCode => bounds.hashCode;
|
int get hashCode => bounds.hashCode;
|
||||||
|
|
||||||
MapState copyWith({
|
MapState copyWith({
|
||||||
LngLatBounds? bounds,
|
LatLngBounds? bounds,
|
||||||
ThemeMode? themeMode,
|
ThemeMode? themeMode,
|
||||||
bool? onlyFavorites,
|
bool? onlyFavorites,
|
||||||
bool? includeArchived,
|
bool? includeArchived,
|
||||||
@@ -64,7 +64,7 @@ class MapState {
|
|||||||
class MapStateNotifier extends Notifier<MapState> {
|
class MapStateNotifier extends Notifier<MapState> {
|
||||||
MapStateNotifier();
|
MapStateNotifier();
|
||||||
|
|
||||||
bool setBounds(LngLatBounds bounds) {
|
bool setBounds(LatLngBounds bounds) {
|
||||||
if (state.bounds == bounds) {
|
if (state.bounds == bounds) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -113,14 +113,14 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
|
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
|
||||||
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
|
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
|
||||||
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
|
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
|
||||||
bounds: const LngLatBounds(longitudeWest: 0, longitudeEast: 0, latitudeSouth: 0, latitudeNorth: 0),
|
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This provider watches the markers from the map service and serves the markers.
|
// This provider watches the markers from the map service and serves the markers.
|
||||||
// It should be used only after the map service provider is overridden
|
// It should be used only after the map service provider is overridden
|
||||||
final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LngLatBounds?>((ref, bounds) async {
|
final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LatLngBounds?>((ref, bounds) async {
|
||||||
final mapService = ref.watch(mapServiceProvider);
|
final mapService = ref.watch(mapServiceProvider);
|
||||||
final markers = await mapService.getMarkers(bounds);
|
final markers = await mapService.getMarkers(bounds);
|
||||||
final features = List.filled(markers.length, const <String, dynamic>{});
|
final features = List.filled(markers.length, const <String, dynamic>{});
|
||||||
@@ -131,7 +131,7 @@ final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LngLatBoun
|
|||||||
'id': marker.assetId,
|
'id': marker.assetId,
|
||||||
'geometry': {
|
'geometry': {
|
||||||
'type': 'Point',
|
'type': 'Point',
|
||||||
'coordinates': [marker.location.lon, marker.location.lat],
|
'coordinates': [marker.location.longitude, marker.location.latitude],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -19,10 +20,27 @@ import 'package:immich_mobile/utils/async_mutex.dart';
|
|||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
class CustomSourceProperties implements SourceProperties {
|
||||||
|
final Map<String, dynamic> data;
|
||||||
|
const CustomSourceProperties({required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"type": "geojson",
|
||||||
|
"data": data,
|
||||||
|
// "cluster": true,
|
||||||
|
// "clusterRadius": 1,
|
||||||
|
// "clusterMinPoints": 5,
|
||||||
|
// "tolerance": 0.1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class DriftMap extends ConsumerStatefulWidget {
|
class DriftMap extends ConsumerStatefulWidget {
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
const DriftMap({super.key, this.initialLocation});
|
const DriftMap({super.key, this.initialLocation});
|
||||||
|
|
||||||
@@ -31,7 +49,7 @@ class DriftMap extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DriftMapState extends ConsumerState<DriftMap> {
|
class _DriftMapState extends ConsumerState<DriftMap> {
|
||||||
MapController? mapController;
|
MapLibreMapController? mapController;
|
||||||
final _reloadMutex = AsyncMutex();
|
final _reloadMutex = AsyncMutex();
|
||||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||||
@@ -51,7 +69,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMapCreated(MapController controller) {
|
void onMapCreated(MapLibreMapController controller) {
|
||||||
mapController = controller;
|
mapController = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,23 +81,43 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await controller.style!.addSource(
|
await controller.addSource(
|
||||||
GeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode({'type': 'FeatureCollection', 'features': []})),
|
MapUtils.defaultSourceId,
|
||||||
|
const CustomSourceProperties(data: {'type': 'FeatureCollection', 'features': []}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await controller.style!.addLayer(
|
if (Platform.isAndroid) {
|
||||||
const HeatmapStyleLayer(
|
await controller.addCircleLayer(
|
||||||
id: MapUtils.defaultHeatMapLayerId,
|
MapUtils.defaultSourceId,
|
||||||
sourceId: MapUtils.defaultSourceId,
|
MapUtils.defaultHeatMapLayerId,
|
||||||
paint: MapUtils.defaultHeatmapLayerPaint,
|
const CircleLayerProperties(
|
||||||
|
circleRadius: 10,
|
||||||
|
circleColor: "rgba(150,86,34,0.7)",
|
||||||
|
circleBlur: 1.0,
|
||||||
|
circleOpacity: 0.7,
|
||||||
|
circleStrokeWidth: 0.1,
|
||||||
|
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||||
|
circleStrokeOpacity: 0.7,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_debouncer.run(() => setBounds(forceReload: true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMapEvent(MapEvent event) {
|
if (Platform.isIOS) {
|
||||||
if (event is! MapEventCameraIdle || !mounted) return;
|
await controller.addHeatmapLayer(
|
||||||
|
MapUtils.defaultSourceId,
|
||||||
|
MapUtils.defaultHeatMapLayerId,
|
||||||
|
MapUtils.defaultHeatmapLayerProperties,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_debouncer.run(() => setBounds(forceReload: true));
|
||||||
|
controller.addListener(onMapMoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onMapMoved() {
|
||||||
|
if (mapController!.isCameraMoving || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_debouncer.run(setBounds);
|
_debouncer.run(setBounds);
|
||||||
}
|
}
|
||||||
@@ -98,7 +136,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final bounds = controller.getVisibleRegion();
|
final bounds = await controller.getVisibleRegion();
|
||||||
unawaited(
|
unawaited(
|
||||||
_reloadMutex.run(() async {
|
_reloadMutex.run(() async {
|
||||||
if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) {
|
if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) {
|
||||||
@@ -115,7 +153,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await controller.style!.updateGeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode(markers));
|
await controller.setGeoJsonSource(MapUtils.defaultSourceId, markers);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onZoomToLocation() async {
|
Future<void> onZoomToLocation() async {
|
||||||
@@ -135,9 +173,8 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
final controller = mapController;
|
final controller = mapController;
|
||||||
if (controller != null && location != null) {
|
if (controller != null && location != null) {
|
||||||
await controller.animateCamera(
|
await controller.animateCamera(
|
||||||
center: Geographic(lat: location.latitude, lon: location.longitude),
|
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel),
|
||||||
zoom: MapUtils.mapZoomToAssetLevel,
|
duration: const Duration(milliseconds: 800),
|
||||||
nativeDuration: Durations.extralong2,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,12 +183,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_Map(
|
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||||
initialLocation: widget.initialLocation,
|
|
||||||
onMapCreated: onMapCreated,
|
|
||||||
onMapReady: onMapReady,
|
|
||||||
onMapEvent: onMapEvent,
|
|
||||||
),
|
|
||||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||||
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||||
],
|
],
|
||||||
@@ -160,13 +192,13 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Map extends StatelessWidget {
|
class _Map extends StatelessWidget {
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady, required this.onMapEvent});
|
const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady});
|
||||||
|
|
||||||
|
final MapCreatedCallback onMapCreated;
|
||||||
|
|
||||||
final void Function(MapController) onMapCreated;
|
|
||||||
final VoidCallback onMapReady;
|
final VoidCallback onMapReady;
|
||||||
final void Function(MapEvent) onMapEvent;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -174,15 +206,16 @@ class _Map extends StatelessWidget {
|
|||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
mapBuilder: (style) => style.widgetWhen(
|
mapBuilder: (style) => style.widgetWhen(
|
||||||
onData: (style) => MapLibreMap(
|
onData: (style) => MapLibreMap(
|
||||||
options: MapOptions(
|
initialCameraPosition: initialLocation == null
|
||||||
initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0),
|
? const CameraPosition(target: LatLng(0, 0), zoom: 0)
|
||||||
initZoom: initialLocation == null ? 0 : MapUtils.mapZoomToAssetLevel,
|
: CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel),
|
||||||
initStyle: style,
|
compassEnabled: false,
|
||||||
gestures: const MapGestures.all(rotate: false),
|
rotateGesturesEnabled: false,
|
||||||
),
|
styleString: style,
|
||||||
onMapCreated: onMapCreated,
|
onMapCreated: onMapCreated,
|
||||||
onStyleLoaded: (_) => onMapReady(),
|
onStyleLoadedCallback: onMapReady,
|
||||||
onEvent: onMapEvent,
|
attributionButtonPosition: AttributionButtonPosition.topRight,
|
||||||
|
attributionButtonMargins: const Point(8, kToolbarHeight),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class MapUtils {
|
class MapUtils {
|
||||||
static final Logger _logger = Logger("MapUtils");
|
static final Logger _logger = Logger("MapUtils");
|
||||||
@@ -12,37 +13,49 @@ class MapUtils {
|
|||||||
static const mapZoomToAssetLevel = 12.0;
|
static const mapZoomToAssetLevel = 12.0;
|
||||||
static const defaultSourceId = 'asset-map-markers';
|
static const defaultSourceId = 'asset-map-markers';
|
||||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
||||||
static const defaultHeatmapLayerPaint = <String, Object>{
|
static var markerCompleter = Completer()..complete();
|
||||||
'heatmap-color': [
|
|
||||||
'interpolate',
|
static const defaultCircleLayerLayerProperties = CircleLayerProperties(
|
||||||
['linear'],
|
circleRadius: 10,
|
||||||
['heatmap-density'],
|
circleColor: "rgba(150,86,34,0.7)",
|
||||||
|
circleBlur: 1.0,
|
||||||
|
circleOpacity: 0.7,
|
||||||
|
circleStrokeWidth: 0.1,
|
||||||
|
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||||
|
circleStrokeOpacity: 0.7,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const defaultHeatmapLayerProperties = HeatmapLayerProperties(
|
||||||
|
heatmapColor: [
|
||||||
|
Expressions.interpolate,
|
||||||
|
["linear"],
|
||||||
|
["heatmap-density"],
|
||||||
0.0,
|
0.0,
|
||||||
'rgba(103,58,183,0.0)',
|
"rgba(103,58,183,0.0)",
|
||||||
0.3,
|
0.3,
|
||||||
'rgb(103,58,183)',
|
"rgb(103,58,183)",
|
||||||
0.5,
|
0.5,
|
||||||
'rgb(33,149,243)',
|
"rgb(33,149,243)",
|
||||||
0.7,
|
0.7,
|
||||||
'rgb(76,175,79)',
|
"rgb(76,175,79)",
|
||||||
0.95,
|
0.95,
|
||||||
'rgb(255,235,59)',
|
"rgb(255,235,59)",
|
||||||
1.0,
|
1.0,
|
||||||
'rgb(255,86,34)',
|
"rgb(255,86,34)",
|
||||||
],
|
],
|
||||||
'heatmap-intensity': [
|
heatmapIntensity: [
|
||||||
'interpolate',
|
Expressions.interpolate,
|
||||||
['linear'],
|
["linear"],
|
||||||
['zoom'],
|
[Expressions.zoom],
|
||||||
0,
|
0,
|
||||||
0.5,
|
0.5,
|
||||||
9,
|
9,
|
||||||
2,
|
2,
|
||||||
],
|
],
|
||||||
'heatmap-radius': [
|
heatmapRadius: [
|
||||||
'interpolate',
|
Expressions.interpolate,
|
||||||
['linear'],
|
["linear"],
|
||||||
['zoom'],
|
[Expressions.zoom],
|
||||||
0,
|
0,
|
||||||
4,
|
4,
|
||||||
4,
|
4,
|
||||||
@@ -50,8 +63,8 @@ class MapUtils {
|
|||||||
9,
|
9,
|
||||||
16,
|
16,
|
||||||
],
|
],
|
||||||
'heatmap-opacity': 0.7,
|
heatmapOpacity: 0.7,
|
||||||
};
|
);
|
||||||
|
|
||||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
|||||||
@@ -29,42 +29,12 @@ import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
||||||
|
|
||||||
class _TimelineRestorationState extends ChangeNotifier {
|
class Timeline extends StatelessWidget {
|
||||||
int? _restoreAssetIndex;
|
|
||||||
bool _shouldRestoreAssetPosition = false;
|
|
||||||
|
|
||||||
int? get restoreAssetIndex => _restoreAssetIndex;
|
|
||||||
bool get shouldRestoreAssetPosition => _shouldRestoreAssetPosition;
|
|
||||||
|
|
||||||
void setRestoreAssetIndex(int? index) {
|
|
||||||
_restoreAssetIndex = index;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setShouldRestoreAssetPosition(bool should) {
|
|
||||||
_shouldRestoreAssetPosition = should;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearRestoreAssetIndex() {
|
|
||||||
_restoreAssetIndex = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineRestorationProvider extends InheritedNotifier<_TimelineRestorationState> {
|
|
||||||
const _TimelineRestorationProvider({required super.notifier, required super.child});
|
|
||||||
|
|
||||||
static _TimelineRestorationState of(BuildContext context) {
|
|
||||||
return context.dependOnInheritedWidgetOfExactType<_TimelineRestorationProvider>()!.notifier!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Timeline extends StatefulWidget {
|
|
||||||
const Timeline({
|
const Timeline({
|
||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
|
this.bottomSliverWidget,
|
||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator = false,
|
||||||
this.withStack = false,
|
this.withStack = false,
|
||||||
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
||||||
@@ -79,6 +49,7 @@ class Timeline extends StatefulWidget {
|
|||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
final double? topSliverWidgetHeight;
|
final double? topSliverWidgetHeight;
|
||||||
|
final Widget? bottomSliverWidget;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
@@ -90,68 +61,39 @@ class Timeline extends StatefulWidget {
|
|||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
final bool persistentBottomBar;
|
final bool persistentBottomBar;
|
||||||
|
|
||||||
@override
|
|
||||||
State<Timeline> createState() => _TimelineState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimelineState extends State<Timeline> {
|
|
||||||
double? _lastWidth;
|
|
||||||
late final _TimelineRestorationState _restorationState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_restorationState = _TimelineRestorationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_restorationState.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (_, constraints) {
|
builder: (_, constraints) => ProviderScope(
|
||||||
if (_lastWidth != null && _lastWidth != constraints.maxWidth) {
|
|
||||||
_restorationState.setShouldRestoreAssetPosition(true);
|
|
||||||
}
|
|
||||||
_lastWidth = constraints.maxWidth;
|
|
||||||
return _TimelineRestorationProvider(
|
|
||||||
notifier: _restorationState,
|
|
||||||
child: ProviderScope(
|
|
||||||
key: ValueKey(_lastWidth),
|
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineArgsProvider.overrideWith(
|
timelineArgsProvider.overrideWith(
|
||||||
(ref) => TimelineArgs(
|
(ref) => TimelineArgs(
|
||||||
maxWidth: constraints.maxWidth,
|
maxWidth: constraints.maxWidth,
|
||||||
maxHeight: constraints.maxHeight,
|
maxHeight: constraints.maxHeight,
|
||||||
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
||||||
showStorageIndicator: widget.showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
withStack: widget.withStack,
|
withStack: withStack,
|
||||||
groupBy: widget.groupBy,
|
groupBy: groupBy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
|
||||||
],
|
],
|
||||||
child: _SliverTimeline(
|
child: _SliverTimeline(
|
||||||
key: const ValueKey('_sliver_timeline'),
|
topSliverWidget: topSliverWidget,
|
||||||
topSliverWidget: widget.topSliverWidget,
|
topSliverWidgetHeight: topSliverWidgetHeight,
|
||||||
topSliverWidgetHeight: widget.topSliverWidgetHeight,
|
bottomSliverWidget: bottomSliverWidget,
|
||||||
appBar: widget.appBar,
|
appBar: appBar,
|
||||||
bottomSheet: widget.bottomSheet,
|
bottomSheet: bottomSheet,
|
||||||
withScrubber: widget.withScrubber,
|
withScrubber: withScrubber,
|
||||||
persistentBottomBar: widget.persistentBottomBar,
|
persistentBottomBar: persistentBottomBar,
|
||||||
snapToMonth: widget.snapToMonth,
|
snapToMonth: snapToMonth,
|
||||||
initialScrollOffset: widget.initialScrollOffset,
|
initialScrollOffset: initialScrollOffset,
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -170,25 +112,28 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier {
|
|||||||
|
|
||||||
class _SliverTimeline extends ConsumerStatefulWidget {
|
class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
const _SliverTimeline({
|
const _SliverTimeline({
|
||||||
super.key,
|
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
|
this.bottomSliverWidget,
|
||||||
this.appBar,
|
this.appBar,
|
||||||
this.bottomSheet,
|
this.bottomSheet,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
this.persistentBottomBar = false,
|
this.persistentBottomBar = false,
|
||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
|
this.maxWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
final double? topSliverWidgetHeight;
|
final double? topSliverWidgetHeight;
|
||||||
|
final Widget? bottomSliverWidget;
|
||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
final bool persistentBottomBar;
|
final bool persistentBottomBar;
|
||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
final double? initialScrollOffset;
|
final double? initialScrollOffset;
|
||||||
|
final double? maxWidth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
@@ -207,6 +152,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
int _perRow = 4;
|
int _perRow = 4;
|
||||||
double _scaleFactor = 3.0;
|
double _scaleFactor = 3.0;
|
||||||
double _baseScaleFactor = 3.0;
|
double _baseScaleFactor = 3.0;
|
||||||
|
int? _restoreAssetIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -225,6 +171,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _SliverTimeline oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.maxWidth != oldWidget.maxWidth) {
|
||||||
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
|
asyncSegments.whenData((segments) {
|
||||||
|
final index = _getCurrentAssetIndex(segments);
|
||||||
|
// Refresh to wait for new segments to be generated with the updated width before restoring the scroll position
|
||||||
|
final _ = ref.refresh(timelineArgsProvider);
|
||||||
|
_restoreAssetIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ScrollToTopEvent():
|
case ScrollToTopEvent():
|
||||||
@@ -242,21 +202,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMultiSelectionToggled(_, bool isEnabled) {
|
|
||||||
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _restoreAssetPosition(_) {
|
void _restoreAssetPosition(_) {
|
||||||
final restorationState = _TimelineRestorationProvider.of(context);
|
if (_restoreAssetIndex == null) return;
|
||||||
if (!restorationState.shouldRestoreAssetPosition || restorationState.restoreAssetIndex == null) return;
|
|
||||||
|
|
||||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
asyncSegments.whenData((segments) {
|
asyncSegments.whenData((segments) {
|
||||||
final targetSegment = segments.lastWhereOrNull(
|
final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!);
|
||||||
(segment) => segment.firstAssetIndex <= restorationState.restoreAssetIndex!,
|
|
||||||
);
|
|
||||||
if (targetSegment != null) {
|
if (targetSegment != null) {
|
||||||
final assetIndexInSegment = restorationState.restoreAssetIndex! - targetSegment.firstAssetIndex;
|
final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex;
|
||||||
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
final newColumnCount = ref.read(timelineArgsProvider).columnCount;
|
||||||
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
|
||||||
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
|
||||||
@@ -268,7 +221,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
restorationState.clearRestoreAssetIndex();
|
_restoreAssetIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMultiSelectionToggled(_, bool isEnabled) {
|
||||||
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _getCurrentAssetIndex(List<Segment> segments) {
|
int? _getCurrentAssetIndex(List<Segment> segments) {
|
||||||
@@ -456,6 +413,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
addRepaintBoundaries: false,
|
addRepaintBoundaries: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.bottomSliverWidget != null) widget.bottomSliverWidget!,
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
|
SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -478,14 +436,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: NotificationListener<ScrollEndNotification>(
|
|
||||||
onNotification: (notification) {
|
|
||||||
final currentIndex = _getCurrentAssetIndex(segments);
|
|
||||||
if (currentIndex != null && mounted) {
|
|
||||||
_TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
child: RawGestureDetector(
|
child: RawGestureDetector(
|
||||||
gestures: {
|
gestures: {
|
||||||
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
||||||
@@ -498,17 +448,15 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
scale.onUpdate = (details) {
|
scale.onUpdate = (details) {
|
||||||
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
||||||
final newPerRow = 7 - newScaleFactor.toInt();
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
|
||||||
|
|
||||||
if (newPerRow != _perRow) {
|
if (newPerRow != _perRow) {
|
||||||
final restorationState = _TimelineRestorationProvider.of(context);
|
final targetAssetIndex = _getCurrentAssetIndex(segments);
|
||||||
setState(() {
|
setState(() {
|
||||||
_scaleFactor = newScaleFactor;
|
_scaleFactor = newScaleFactor;
|
||||||
_perRow = newPerRow;
|
_perRow = newPerRow;
|
||||||
|
_restoreAssetIndex = targetAssetIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
restorationState.setRestoreAssetIndex(targetAssetIndex);
|
|
||||||
restorationState.setShouldRestoreAssetPosition(true);
|
|
||||||
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -527,7 +475,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
timeline,
|
timeline,
|
||||||
if (isMultiSelectStatusVisible)
|
if (isBottomWidgetVisible)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: MediaQuery.paddingOf(context).top,
|
top: MediaQuery.paddingOf(context).top,
|
||||||
left: 25,
|
left: 25,
|
||||||
@@ -541,7 +489,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -343,6 +343,22 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> setAlbumCover(ActionSource source, String albumId) async {
|
||||||
|
final assets = _getAssets(source);
|
||||||
|
final asset = assets.first;
|
||||||
|
if (asset is! RemoteAsset) {
|
||||||
|
return const ActionResult(count: 1, success: false, error: 'Asset must be remote');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _service.setAlbumCover(albumId, asset.id);
|
||||||
|
return const ActionResult(count: 1, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to set album cover', error, stack);
|
||||||
|
return ActionResult(count: 1, success: false, error: error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<ActionResult> updateDescription(ActionSource source, String description) async {
|
Future<ActionResult> updateDescription(ActionSource source, String description) async {
|
||||||
final ids = _getRemoteIdsForSource(source);
|
final ids = _getRemoteIdsForSource(source);
|
||||||
if (ids.length != 1) {
|
if (ids.length != 1) {
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
|
|||||||
|
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
|
|
||||||
Future<bool> upload(XFile file) async {
|
Future<bool> upload(XFile file, {String? fileName}) async {
|
||||||
state = state.copyWith(status: UploadProfileStatus.loading);
|
state = state.copyWith(status: UploadProfileStatus.loading);
|
||||||
|
|
||||||
var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes());
|
var profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes());
|
||||||
|
|
||||||
if (profileImagePath != null) {
|
if (profileImagePath != null) {
|
||||||
dPrint(() => "Successfully upload profile image");
|
dPrint(() => "Successfully upload profile image");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/stack.model.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final assetApiRepositoryProvider = Provider(
|
final assetApiRepositoryProvider = Provider(
|
||||||
@@ -62,8 +62,8 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, Geographic location) async {
|
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.lat, longitude: location.lon));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart';
|
import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart';
|
import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart';
|
import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
||||||
@@ -123,7 +124,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/services/local_auth.service.dart';
|
import 'package:immich_mobile/services/local_auth.service.dart';
|
||||||
import 'package:immich_mobile/services/secure_storage.service.dart';
|
import 'package:immich_mobile/services/secure_storage.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
@@ -198,6 +199,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: EditImageRoute.page),
|
AutoRoute(page: EditImageRoute.page),
|
||||||
AutoRoute(page: CropImageRoute.page),
|
AutoRoute(page: CropImageRoute.page),
|
||||||
AutoRoute(page: FilterImageRoute.page),
|
AutoRoute(page: FilterImageRoute.page),
|
||||||
|
AutoRoute(page: ProfilePictureCropRoute.page),
|
||||||
CustomRoute(
|
CustomRoute(
|
||||||
page: FavoritesRoute.page,
|
page: FavoritesRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
|||||||
@@ -1226,7 +1226,7 @@ class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
|||||||
class DriftMapRoute extends PageRouteInfo<DriftMapRouteArgs> {
|
class DriftMapRoute extends PageRouteInfo<DriftMapRouteArgs> {
|
||||||
DriftMapRoute({
|
DriftMapRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
Geographic? initialLocation,
|
LatLng? initialLocation,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
DriftMapRoute.name,
|
DriftMapRoute.name,
|
||||||
@@ -1252,7 +1252,7 @@ class DriftMapRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -1461,7 +1461,7 @@ class DriftPlaceDetailRouteArgs {
|
|||||||
class DriftPlaceRoute extends PageRouteInfo<DriftPlaceRouteArgs> {
|
class DriftPlaceRoute extends PageRouteInfo<DriftPlaceRouteArgs> {
|
||||||
DriftPlaceRoute({
|
DriftPlaceRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
Geographic? currentLocation,
|
LatLng? currentLocation,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
DriftPlaceRoute.name,
|
DriftPlaceRoute.name,
|
||||||
@@ -1490,7 +1490,7 @@ class DriftPlaceRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Geographic? currentLocation;
|
final LatLng? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2011,7 +2011,7 @@ class MainTimelineRoute extends PageRouteInfo<void> {
|
|||||||
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||||
MapLocationPickerRoute({
|
MapLocationPickerRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
Geographic initialLatLng = const Geographic(lat: 0, lon: 0),
|
LatLng initialLatLng = const LatLng(0, 0),
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
MapLocationPickerRoute.name,
|
MapLocationPickerRoute.name,
|
||||||
@@ -2041,12 +2041,12 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
|||||||
class MapLocationPickerRouteArgs {
|
class MapLocationPickerRouteArgs {
|
||||||
const MapLocationPickerRouteArgs({
|
const MapLocationPickerRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
this.initialLatLng = const Geographic(lat: 0, lon: 0),
|
this.initialLatLng = const LatLng(0, 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Geographic initialLatLng;
|
final LatLng initialLatLng;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2057,11 +2057,8 @@ class MapLocationPickerRouteArgs {
|
|||||||
/// generated route for
|
/// generated route for
|
||||||
/// [MapPage]
|
/// [MapPage]
|
||||||
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
||||||
MapRoute({
|
MapRoute({Key? key, LatLng? initialLocation, List<PageRouteInfo>? children})
|
||||||
Key? key,
|
: super(
|
||||||
Geographic? initialLocation,
|
|
||||||
List<PageRouteInfo>? children,
|
|
||||||
}) : super(
|
|
||||||
MapRoute.name,
|
MapRoute.name,
|
||||||
args: MapRouteArgs(key: key, initialLocation: initialLocation),
|
args: MapRouteArgs(key: key, initialLocation: initialLocation),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
@@ -2085,7 +2082,7 @@ class MapRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Geographic? initialLocation;
|
final LatLng? initialLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2406,7 +2403,7 @@ class PinAuthRouteArgs {
|
|||||||
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||||
PlacesCollectionRoute({
|
PlacesCollectionRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
Geographic? currentLocation,
|
LatLng? currentLocation,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
PlacesCollectionRoute.name,
|
PlacesCollectionRoute.name,
|
||||||
@@ -2438,7 +2435,7 @@ class PlacesCollectionRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Geographic? currentLocation;
|
final LatLng? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2446,6 +2443,44 @@ class PlacesCollectionRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [ProfilePictureCropPage]
|
||||||
|
class ProfilePictureCropRoute
|
||||||
|
extends PageRouteInfo<ProfilePictureCropRouteArgs> {
|
||||||
|
ProfilePictureCropRoute({
|
||||||
|
Key? key,
|
||||||
|
required BaseAsset asset,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
ProfilePictureCropRoute.name,
|
||||||
|
args: ProfilePictureCropRouteArgs(key: key, asset: asset),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'ProfilePictureCropRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<ProfilePictureCropRouteArgs>();
|
||||||
|
return ProfilePictureCropPage(key: args.key, asset: args.asset);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfilePictureCropRouteArgs {
|
||||||
|
const ProfilePictureCropRouteArgs({this.key, required this.asset});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final BaseAsset asset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ProfilePictureCropRouteArgs{key: $key, asset: $asset}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [RecentlyTakenPage]
|
/// [RecentlyTakenPage]
|
||||||
class RecentlyTakenRoute extends PageRouteInfo<void> {
|
class RecentlyTakenRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/utils/timezone.dart';
|
import 'package:immich_mobile/utils/timezone.dart';
|
||||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:maplibre/maplibre.dart' as maplibre;
|
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
final actionServiceProvider = Provider<ActionService>(
|
final actionServiceProvider = Provider<ActionService>(
|
||||||
@@ -131,12 +131,12 @@ class ActionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> editLocation(List<String> remoteIds, BuildContext context) async {
|
Future<bool> editLocation(List<String> remoteIds, BuildContext context) async {
|
||||||
maplibre.Geographic? initialLatLng;
|
maplibre.LatLng? initialLatLng;
|
||||||
if (remoteIds.length == 1) {
|
if (remoteIds.length == 1) {
|
||||||
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||||
|
|
||||||
if (exif?.latitude != null && exif?.longitude != null) {
|
if (exif?.latitude != null && exif?.longitude != null) {
|
||||||
initialLatLng = maplibre.Geographic(lat: exif!.latitude!, lon: exif.longitude!);
|
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +240,12 @@ class ActionService {
|
|||||||
return _downloadRepository.downloadAllAssets(assets);
|
return _downloadRepository.downloadAllAssets(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setAlbumCover(String albumId, String assetId) async {
|
||||||
|
final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, thumbnailAssetId: assetId);
|
||||||
|
await _remoteAlbumRepository.update(updatedAlbum);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
||||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||||
if (deletedIds.isEmpty) {
|
if (deletedIds.isEmpty) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ enum AppSettingsEnum<T> {
|
|||||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
|
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
|
||||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, "autoPlayVideo", true),
|
autoPlayVideo<bool>(StoreKey.autoPlayVideo, "autoPlayVideo", true),
|
||||||
|
tapToNavigate<bool>(StoreKey.tapToNavigate, "tapToNavigate", false),
|
||||||
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
|
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
|
||||||
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
||||||
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
|
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
||||||
@@ -236,12 +236,12 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Asset>?> changeLocation(List<Asset> assets, Geographic location) async {
|
Future<List<Asset>?> changeLocation(List<Asset> assets, LatLng location) async {
|
||||||
try {
|
try {
|
||||||
await updateAssets(assets, UpdateAssetDto(latitude: location.lat, longitude: location.lon));
|
await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude));
|
||||||
|
|
||||||
for (var element in assets) {
|
for (var element in assets) {
|
||||||
element.exifInfo = element.exifInfo?.copyWith(latitude: location.lat, longitude: location.lon);
|
element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _syncService.upsertAssetsWithExif(assets);
|
await _syncService.upsertAssetsWithExif(assets);
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/user_agent.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class MapService with ErrorLoggerMixin {
|
class MapService with ErrorLoggerMixin {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
@override
|
@override
|
||||||
final logger = Logger("MapService");
|
final logger = Logger("MapService");
|
||||||
|
|
||||||
MapService(this._apiService);
|
MapService(this._apiService) {
|
||||||
|
_setMapUserAgentHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setMapUserAgentHeader() async {
|
||||||
|
final userAgent = await getUserAgentString();
|
||||||
|
await setHttpHeaders({'User-Agent': userAgent});
|
||||||
|
}
|
||||||
|
|
||||||
Future<Iterable<MapMarker>> getMapMarkers({
|
Future<Iterable<MapMarker>> getMapMarkers({
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class SharedLinkService {
|
|||||||
required bool allowUpload,
|
required bool allowUpload,
|
||||||
String? description,
|
String? description,
|
||||||
String? password,
|
String? password,
|
||||||
|
String? slug,
|
||||||
String? albumId,
|
String? albumId,
|
||||||
List<String>? assetIds,
|
List<String>? assetIds,
|
||||||
DateTime? expiresAt,
|
DateTime? expiresAt,
|
||||||
@@ -54,6 +55,7 @@ class SharedLinkService {
|
|||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
description: description,
|
description: description,
|
||||||
password: password,
|
password: password,
|
||||||
|
slug: slug,
|
||||||
);
|
);
|
||||||
} else if (assetIds != null) {
|
} else if (assetIds != null) {
|
||||||
dto = SharedLinkCreateDto(
|
dto = SharedLinkCreateDto(
|
||||||
@@ -64,6 +66,7 @@ class SharedLinkService {
|
|||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
description: description,
|
description: description,
|
||||||
password: password,
|
password: password,
|
||||||
|
slug: slug,
|
||||||
assetIds: assetIds,
|
assetIds: assetIds,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -88,6 +91,7 @@ class SharedLinkService {
|
|||||||
bool? changeExpiry = false,
|
bool? changeExpiry = false,
|
||||||
String? description,
|
String? description,
|
||||||
String? password,
|
String? password,
|
||||||
|
String? slug,
|
||||||
DateTime? expiresAt,
|
DateTime? expiresAt,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
@@ -100,6 +104,7 @@ class SharedLinkService {
|
|||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
description: description,
|
description: description,
|
||||||
password: password,
|
password: password,
|
||||||
|
slug: slug,
|
||||||
changeExpiryTime: changeExpiry,
|
changeExpiryTime: changeExpiry,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||||
@@ -42,6 +44,7 @@ class ActionButtonContext {
|
|||||||
final bool isCasting;
|
final bool isCasting;
|
||||||
final TimelineOrigin timelineOrigin;
|
final TimelineOrigin timelineOrigin;
|
||||||
final ThemeData? originalTheme;
|
final ThemeData? originalTheme;
|
||||||
|
final int selectedCount;
|
||||||
|
|
||||||
const ActionButtonContext({
|
const ActionButtonContext({
|
||||||
required this.asset,
|
required this.asset,
|
||||||
@@ -56,6 +59,7 @@ class ActionButtonContext {
|
|||||||
this.isCasting = false,
|
this.isCasting = false,
|
||||||
this.timelineOrigin = TimelineOrigin.main,
|
this.timelineOrigin = TimelineOrigin.main,
|
||||||
this.originalTheme,
|
this.originalTheme,
|
||||||
|
this.selectedCount = 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +69,9 @@ enum ActionButtonType {
|
|||||||
share,
|
share,
|
||||||
shareLink,
|
shareLink,
|
||||||
cast,
|
cast,
|
||||||
|
setAlbumCover,
|
||||||
similarPhotos,
|
similarPhotos,
|
||||||
|
setProfilePicture,
|
||||||
viewInTimeline,
|
viewInTimeline,
|
||||||
download,
|
download,
|
||||||
upload,
|
upload,
|
||||||
@@ -134,6 +140,11 @@ enum ActionButtonType {
|
|||||||
context.isOwner && //
|
context.isOwner && //
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
context.currentAlbum != null,
|
context.currentAlbum != null,
|
||||||
|
ActionButtonType.setAlbumCover =>
|
||||||
|
context.isOwner && //
|
||||||
|
!context.isInLockedView && //
|
||||||
|
context.currentAlbum != null && //
|
||||||
|
context.selectedCount == 1,
|
||||||
ActionButtonType.unstack =>
|
ActionButtonType.unstack =>
|
||||||
context.isOwner && //
|
context.isOwner && //
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
@@ -146,6 +157,10 @@ enum ActionButtonType {
|
|||||||
ActionButtonType.similarPhotos =>
|
ActionButtonType.similarPhotos =>
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
context.asset is RemoteAsset,
|
context.asset is RemoteAsset,
|
||||||
|
ActionButtonType.setProfilePicture =>
|
||||||
|
!context.isInLockedView && //
|
||||||
|
context.asset is RemoteAsset && //
|
||||||
|
context.isOwner,
|
||||||
ActionButtonType.openInfo => true,
|
ActionButtonType.openInfo => true,
|
||||||
ActionButtonType.viewInTimeline =>
|
ActionButtonType.viewInTimeline =>
|
||||||
context.timelineOrigin != TimelineOrigin.main &&
|
context.timelineOrigin != TimelineOrigin.main &&
|
||||||
@@ -213,6 +228,12 @@ enum ActionButtonType {
|
|||||||
iconOnly: iconOnly,
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
|
ActionButtonType.setAlbumCover => SetAlbumCoverActionButton(
|
||||||
|
albumId: context.currentAlbum!.id,
|
||||||
|
source: context.source,
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
),
|
||||||
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
|
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
|
||||||
@@ -220,6 +241,11 @@ enum ActionButtonType {
|
|||||||
iconOnly: iconOnly,
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
|
ActionButtonType.setProfilePicture => SetProfilePictureActionButton(
|
||||||
|
asset: context.asset,
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
),
|
||||||
ActionButtonType.openInfo => BaseActionButton(
|
ActionButtonType.openInfo => BaseActionButton(
|
||||||
label: 'info'.tr(),
|
label: 'info'.tr(),
|
||||||
iconData: Icons.info_outline,
|
iconData: Icons.info_outline,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Converts a Flutter [Image] widget to a [Uint8List] in PNG format.
|
||||||
|
///
|
||||||
|
/// This function resolves the image stream and converts it to byte data.
|
||||||
|
/// Returns a [Future] that completes with the image bytes or completes with an error
|
||||||
|
/// if the conversion fails.
|
||||||
|
Future<Uint8List> imageToUint8List(Image image) async {
|
||||||
|
final Completer<Uint8List> completer = Completer();
|
||||||
|
image.image
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(
|
||||||
|
ImageStreamListener((ImageInfo info, bool _) {
|
||||||
|
info.image.toByteData(format: ImageByteFormat.png).then((byteData) {
|
||||||
|
if (byteData != null) {
|
||||||
|
completer.complete(byteData.buffer.asUint8List());
|
||||||
|
} else {
|
||||||
|
completer.completeError('Failed to convert image to bytes');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, onError: (exception, stackTrace) => completer.completeError(exception)),
|
||||||
|
);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class MapUtils {
|
class MapUtils {
|
||||||
const MapUtils._();
|
const MapUtils._();
|
||||||
@@ -14,53 +15,46 @@ class MapUtils {
|
|||||||
static const defaultSourceId = 'asset-map-markers';
|
static const defaultSourceId = 'asset-map-markers';
|
||||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
||||||
|
|
||||||
static const defaultHeatMapLayerPaint = <String, Object>{
|
static const defaultHeatMapLayerProperties = HeatmapLayerProperties(
|
||||||
'heatmap-color': [
|
heatmapColor: [
|
||||||
'interpolate',
|
Expressions.interpolate,
|
||||||
['linear'],
|
["linear"],
|
||||||
['heatmap-density'],
|
["heatmap-density"],
|
||||||
0.0,
|
0.0,
|
||||||
'rgba(103,58,183,0.0)',
|
"rgba(103,58,183,0.0)",
|
||||||
0.3,
|
0.3,
|
||||||
'rgb(103,58,183)',
|
"rgb(103,58,183)",
|
||||||
0.5,
|
0.5,
|
||||||
'rgb(33,149,243)',
|
"rgb(33,149,243)",
|
||||||
0.7,
|
0.7,
|
||||||
'rgb(76,175,79)',
|
"rgb(76,175,79)",
|
||||||
0.95,
|
0.95,
|
||||||
'rgb(255,235,59)',
|
"rgb(255,235,59)",
|
||||||
1.0,
|
1.0,
|
||||||
'rgb(255,86,34)',
|
"rgb(255,86,34)",
|
||||||
],
|
],
|
||||||
'heatmap-intensity': [
|
heatmapIntensity: [
|
||||||
'interpolate',
|
Expressions.interpolate, ["linear"], //
|
||||||
['linear'],
|
[Expressions.zoom],
|
||||||
['zoom'],
|
0, 0.5,
|
||||||
0,
|
9, 2,
|
||||||
0.5,
|
|
||||||
9,
|
|
||||||
2,
|
|
||||||
],
|
],
|
||||||
'heatmap-radius': [
|
heatmapRadius: [
|
||||||
'interpolate',
|
Expressions.interpolate, ["linear"], //
|
||||||
['linear'],
|
[Expressions.zoom],
|
||||||
['zoom'],
|
0, 4,
|
||||||
0,
|
4, 8,
|
||||||
4,
|
9, 16,
|
||||||
4,
|
|
||||||
8,
|
|
||||||
9,
|
|
||||||
16,
|
|
||||||
],
|
],
|
||||||
'heatmap-opacity': 0.7,
|
heatmapOpacity: 0.7,
|
||||||
};
|
);
|
||||||
|
|
||||||
static Map<String, dynamic> _addFeature(MapMarker marker) => {
|
static Map<String, dynamic> _addFeature(MapMarker marker) => {
|
||||||
'type': 'Feature',
|
'type': 'Feature',
|
||||||
'id': marker.assetRemoteId,
|
'id': marker.assetRemoteId,
|
||||||
'geometry': {
|
'geometry': {
|
||||||
'type': 'Point',
|
'type': 'Point',
|
||||||
'coordinates': [marker.latLng.lon, marker.latLng.lat],
|
'coordinates': [marker.latLng.longitude, marker.latLng.latitude],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,10 @@ import 'package:immich_mobile/utils/datetime_helpers.dart';
|
|||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
// ignore: import_rule_photo_manager
|
// ignore: import_rule_photo_manager
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
const int targetVersion = 21;
|
const int targetVersion = 22;
|
||||||
|
|
||||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||||
@@ -100,6 +99,10 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 22 && !Store.isBetaTimelineEnabled) {
|
||||||
|
await Store.put(StoreKey.needBetaMigration, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (targetVersion >= 12) {
|
if (targetVersion >= 12) {
|
||||||
await Store.put(StoreKey.version, targetVersion);
|
await Store.put(StoreKey.version, targetVersion);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
sealed class Option<T> {
|
||||||
|
const Option();
|
||||||
|
|
||||||
|
const factory Option.some(T value) = Some<T>;
|
||||||
|
|
||||||
|
const factory Option.none() = None<T>;
|
||||||
|
|
||||||
|
factory Option.fromNullable(T? value) => value != null ? Some(value) : None<T>();
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
bool get isSome => this is Some<T>;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
bool get isNone => this is None<T>;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
T? get unwrapOrNull => switch (this) {
|
||||||
|
Some(:final value) => value,
|
||||||
|
None() => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
U fold<U>(U Function(T value) onSome, U Function() onNone) => switch (this) {
|
||||||
|
Some(:final value) => onSome(value),
|
||||||
|
None() => onNone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => switch (this) {
|
||||||
|
Some(:final value) => 'Some($value)',
|
||||||
|
None() => 'None',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Some<T> extends Option<T> {
|
||||||
|
final T value;
|
||||||
|
|
||||||
|
const Some(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Some<T> && other.value == value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => value.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class None<T> extends Option<T> {
|
||||||
|
const None();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is None<T>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObjectOptionExtension<T> on T? {
|
||||||
|
Option<T> toOption() => Option.fromNullable(this);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:immich_mobile/widgets/common/share_dialog.dart';
|
import 'package:immich_mobile/widgets/common/share_dialog.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
void handleShareAssets(WidgetRef ref, BuildContext context, Iterable<Asset> selection) {
|
void handleShareAssets(WidgetRef ref, BuildContext context, Iterable<Asset> selection) {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -105,12 +105,12 @@ Future<void> handleEditDateTime(WidgetRef ref, BuildContext context, List<Asset>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async {
|
Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async {
|
||||||
Geographic? initialLatLng;
|
LatLng? initialLatLng;
|
||||||
if (selection.length == 1) {
|
if (selection.length == 1) {
|
||||||
final asset = selection.first;
|
final asset = selection.first;
|
||||||
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
||||||
if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) {
|
if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) {
|
||||||
initialLatLng = Geographic(lat: assetWithExif.exifInfo!.latitude!, lon: assetWithExif.exifInfo!.longitude!);
|
initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:immich_mobile/services/stack.service.dart';
|
|||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@@ -405,10 +406,10 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
bottom: false,
|
bottom: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ref
|
ref.watch(renderListProvider).widgetWhen(
|
||||||
.watch(renderListProvider)
|
onLoading: buildLoadingIndicator ?? buildDefaultLoadingIndicator,
|
||||||
.when(
|
onError: (error, _) => Center(child: Text(error.toString())),
|
||||||
data: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null)
|
onData: (data) => data.isEmpty && (buildLoadingIndicator != null || topWidget == null)
|
||||||
? (buildLoadingIndicator ?? buildEmptyIndicator)()
|
? (buildLoadingIndicator ?? buildEmptyIndicator)()
|
||||||
: ImmichAssetGrid(
|
: ImmichAssetGrid(
|
||||||
renderList: data,
|
renderList: data,
|
||||||
@@ -419,8 +420,6 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
showStack: stackEnabled,
|
showStack: stackEnabled,
|
||||||
showDragScrollLabel: dragScrollLabelEnabled,
|
showDragScrollLabel: dragScrollLabelEnabled,
|
||||||
),
|
),
|
||||||
error: (error, _) => Center(child: Text(error.toString())),
|
|
||||||
loading: buildLoadingIndicator ?? buildDefaultLoadingIndicator,
|
|
||||||
),
|
),
|
||||||
if (selectionEnabledHook.value)
|
if (selectionEnabledHook.value)
|
||||||
ControlBottomAppBar(
|
ControlBottomAppBar(
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
|
class TrashDeleteDialog extends StatelessWidget {
|
||||||
|
const TrashDeleteDialog({super.key, required this.count});
|
||||||
|
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
|
title: Text(context.t.permanently_delete),
|
||||||
|
content: ImmichFormattedText(context.t.permanently_delete_assets_prompt(count: count)),
|
||||||
|
actions: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => context.pop(false),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: context.colorScheme.surfaceDim,
|
||||||
|
foregroundColor: context.primaryColor,
|
||||||
|
),
|
||||||
|
child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => context.pop(true),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: context.colorScheme.errorContainer,
|
||||||
|
foregroundColor: context.colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
child: Text(context.t.delete, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,9 +21,7 @@ class CenterPlayButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ColoredBox(
|
return Center(
|
||||||
color: Colors.transparent,
|
|
||||||
child: Center(
|
|
||||||
child: UnconstrainedBox(
|
child: UnconstrainedBox(
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
opacity: show ? 1.0 : 0.0,
|
opacity: show ? 1.0 : 0.0,
|
||||||
@@ -41,7 +39,6 @@ class CenterPlayButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ExifMap extends StatelessWidget {
|
class ExifMap extends StatelessWidget {
|
||||||
@@ -15,7 +15,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
// reusing this component
|
// reusing this component
|
||||||
final String? markerId;
|
final String? markerId;
|
||||||
final String? markerAssetThumbhash;
|
final String? markerAssetThumbhash;
|
||||||
final void Function(MapController)? onMapCreated;
|
final MapCreatedCallback? onMapCreated;
|
||||||
|
|
||||||
const ExifMap({
|
const ExifMap({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -66,7 +66,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return MapThumbnail(
|
return MapThumbnail(
|
||||||
centre: Geographic(lat: exifInfo.latitude ?? 0, lon: exifInfo.longitude ?? 0),
|
centre: LatLng(exifInfo.latitude ?? 0, exifInfo.longitude ?? 0),
|
||||||
height: 150,
|
height: 150,
|
||||||
width: constraints.maxWidth,
|
width: constraints.maxWidth,
|
||||||
zoom: 12.0,
|
zoom: 12.0,
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
Future<Geographic?> showLocationPicker({required BuildContext context, Geographic? initialLatLng}) {
|
Future<LatLng?> showLocationPicker({required BuildContext context, LatLng? initialLatLng}) {
|
||||||
return showDialog<Geographic?>(
|
return showDialog<LatLng?>(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng),
|
builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng),
|
||||||
@@ -17,7 +17,7 @@ Future<Geographic?> showLocationPicker({required BuildContext context, Geographi
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocationPicker extends HookWidget {
|
class _LocationPicker extends HookWidget {
|
||||||
final Geographic? initialLatLng;
|
final LatLng? initialLatLng;
|
||||||
|
|
||||||
const _LocationPicker({this.initialLatLng});
|
const _LocationPicker({this.initialLatLng});
|
||||||
|
|
||||||
@@ -33,9 +33,9 @@ class _LocationPicker extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final latitude = useState(initialLatLng?.lat ?? 0.0);
|
final latitude = useState(initialLatLng?.latitude ?? 0.0);
|
||||||
final longitude = useState(initialLatLng?.lon ?? 0.0);
|
final longitude = useState(initialLatLng?.longitude ?? 0.0);
|
||||||
final latlng = Geographic(lat: latitude.value, lon: longitude.value);
|
final latlng = LatLng(latitude.value, longitude.value);
|
||||||
final latitiudeFocusNode = useFocusNode();
|
final latitiudeFocusNode = useFocusNode();
|
||||||
final longitudeFocusNode = useFocusNode();
|
final longitudeFocusNode = useFocusNode();
|
||||||
final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4));
|
final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4));
|
||||||
@@ -48,10 +48,10 @@ class _LocationPicker extends HookWidget {
|
|||||||
}, [latitude.value, longitude.value]);
|
}, [latitude.value, longitude.value]);
|
||||||
|
|
||||||
Future<void> onMapTap() async {
|
Future<void> onMapTap() async {
|
||||||
final newLatLng = await context.pushRoute<Geographic?>(MapLocationPickerRoute(initialLatLng: latlng));
|
final newLatLng = await context.pushRoute<LatLng?>(MapLocationPickerRoute(initialLatLng: latlng));
|
||||||
if (newLatLng != null) {
|
if (newLatLng != null) {
|
||||||
latitude.value = newLatLng.lat;
|
latitude.value = newLatLng.latitude;
|
||||||
longitude.value = newLatLng.lon;
|
longitude.value = newLatLng.longitude;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ class MapAssetGrid extends HookConsumerWidget {
|
|||||||
final assetCache = useRef<Map<String, Asset>>({});
|
final assetCache = useRef<Map<String, Asset>>({});
|
||||||
|
|
||||||
void handleMapEvents(MapEvent event) async {
|
void handleMapEvents(MapEvent event) async {
|
||||||
if (event is! MapAssetsInBoundsUpdated) return;
|
if (event is MapAssetsInBoundsUpdated) {
|
||||||
|
|
||||||
final assetIds = event.assetRemoteIds;
|
final assetIds = event.assetRemoteIds;
|
||||||
final missingIds = <String>[];
|
final missingIds = <String>[];
|
||||||
final currentAssets = <Asset>[];
|
final currentAssets = <Asset>[];
|
||||||
@@ -80,6 +79,8 @@ class MapAssetGrid extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assetsInBounds.value = currentAssets;
|
assetsInBounds.value = currentAssets;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
||||||
|
|||||||
@@ -33,9 +33,13 @@ class MapBottomSheet extends HookConsumerWidget {
|
|||||||
final isBottomSheetOpened = useRef(false);
|
final isBottomSheetOpened = useRef(false);
|
||||||
|
|
||||||
void handleMapEvents(MapEvent event) async {
|
void handleMapEvents(MapEvent event) async {
|
||||||
if (event is! MapCloseBottomSheet) return;
|
if (event is MapCloseBottomSheet) {
|
||||||
|
await sheetController.animateTo(
|
||||||
await sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut);
|
0.1,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.linearToEaseOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class MapThemePicker extends StatelessWidget {
|
class MapThemePicker extends StatelessWidget {
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
@@ -78,7 +78,7 @@ class _BorderedMapThumbnail extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
centre: const Geographic(lat: 47, lon: 5),
|
centre: const LatLng(47, 5),
|
||||||
onTap: (_, __) => onThemeChange(mode),
|
onTap: (_, __) => onThemeChange(mode),
|
||||||
themeMode: mode,
|
themeMode: mode,
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
|
|||||||
@@ -84,14 +84,9 @@ class _MapThemeOverrideState extends ConsumerState<MapThemeOverride> with Widget
|
|||||||
data: _isDarkTheme
|
data: _isDarkTheme
|
||||||
? getThemeData(colorScheme: appTheme.dark, locale: locale)
|
? getThemeData(colorScheme: appTheme.dark, locale: locale)
|
||||||
: getThemeData(colorScheme: appTheme.light, locale: locale),
|
: getThemeData(colorScheme: appTheme.light, locale: locale),
|
||||||
// Key on _isDarkTheme to force MapLibreMap recreation on theme change,
|
|
||||||
// since initStyle is only applied on creation.
|
|
||||||
child: KeyedSubtree(
|
|
||||||
key: ValueKey(_isDarkTheme),
|
|
||||||
child: widget.mapBuilder.call(
|
child: widget.mapBuilder.call(
|
||||||
ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)),
|
ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:immich_mobile/widgets/map/asset_marker_icon.dart';
|
import 'package:immich_mobile/widgets/map/asset_marker_icon.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
/// A non-interactive thumbnail of a map in the given coordinates with optional markers
|
/// A non-interactive thumbnail of a map in the given coordinates with optional markers
|
||||||
///
|
///
|
||||||
@@ -13,8 +16,8 @@ import 'package:maplibre/maplibre.dart';
|
|||||||
/// [showMarkerPin] to true which would display a marker pin instead. If both are provided,
|
/// [showMarkerPin] to true which would display a marker pin instead. If both are provided,
|
||||||
/// [assetMarkerRemoteId] will take precedence
|
/// [assetMarkerRemoteId] will take precedence
|
||||||
class MapThumbnail extends HookConsumerWidget {
|
class MapThumbnail extends HookConsumerWidget {
|
||||||
final Function(Offset, Geographic)? onTap;
|
final Function(Point<double>, LatLng)? onTap;
|
||||||
final Geographic centre;
|
final LatLng centre;
|
||||||
final String? assetMarkerRemoteId;
|
final String? assetMarkerRemoteId;
|
||||||
final String? assetThumbhash;
|
final String? assetThumbhash;
|
||||||
final bool showMarkerPin;
|
final bool showMarkerPin;
|
||||||
@@ -23,7 +26,7 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
final double width;
|
final double width;
|
||||||
final ThemeMode? themeMode;
|
final ThemeMode? themeMode;
|
||||||
final bool showAttribution;
|
final bool showAttribution;
|
||||||
final void Function(MapController)? onCreated;
|
final MapCreatedCallback? onCreated;
|
||||||
|
|
||||||
const MapThumbnail({
|
const MapThumbnail({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -42,19 +45,26 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final controller = useRef<MapLibreMapController?>(null);
|
||||||
final styleLoaded = useState(false);
|
final styleLoaded = useState(false);
|
||||||
|
|
||||||
Future<void> onStyleLoaded(StyleController style) async {
|
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
||||||
if (showMarkerPin) {
|
controller.value = mapController;
|
||||||
await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png');
|
styleLoaded.value = false;
|
||||||
}
|
onCreated?.call(mapController);
|
||||||
styleLoaded.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEvent(MapEvent event) {
|
Future<void> onStyleLoaded() async {
|
||||||
if (event is MapEventClick && onTap != null) {
|
try {
|
||||||
onTap!(event.screenPoint, event.point);
|
if (showMarkerPin && controller.value != null) {
|
||||||
|
await controller.value?.addMarkerAtLatLng(centre);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Calling methods on the controller after it is disposed will throw an error
|
||||||
|
// We do not have a way to check if the controller is disposed for now
|
||||||
|
// https://github.com/maplibre/flutter-maplibre-gl/issues/192
|
||||||
|
}
|
||||||
|
styleLoaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
@@ -70,44 +80,40 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
width: width,
|
width: width,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
child: style.widgetWhen(
|
child: Stack(
|
||||||
onData: (style) => MapLibreMap(
|
alignment: AlignmentGeometry.topCenter,
|
||||||
options: MapOptions(
|
|
||||||
initCenter: Geographic(lat: centre.lat + 0.002, lon: centre.lon),
|
|
||||||
initZoom: zoom,
|
|
||||||
initStyle: style,
|
|
||||||
gestures: const MapGestures.none(),
|
|
||||||
),
|
|
||||||
onMapCreated: onCreated,
|
|
||||||
onStyleLoaded: onStyleLoaded,
|
|
||||||
onEvent: onEvent,
|
|
||||||
layers: [
|
|
||||||
if (showMarkerPin)
|
|
||||||
MarkerLayer(
|
|
||||||
points: [Feature(geometry: Point(centre))],
|
|
||||||
iconImage: 'mapMarker',
|
|
||||||
iconSize: 0.15,
|
|
||||||
iconAnchor: IconAnchor.bottom,
|
|
||||||
iconAllowOverlap: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
children: [
|
children: [
|
||||||
|
style.widgetWhen(
|
||||||
|
onData: (style) => MapLibreMap(
|
||||||
|
initialCameraPosition: CameraPosition(target: centre, zoom: zoom),
|
||||||
|
styleString: style,
|
||||||
|
onMapCreated: onMapCreated,
|
||||||
|
onStyleLoadedCallback: onStyleLoaded,
|
||||||
|
onMapClick: onTap,
|
||||||
|
doubleClickZoomEnabled: false,
|
||||||
|
dragEnabled: false,
|
||||||
|
zoomGesturesEnabled: false,
|
||||||
|
tiltGesturesEnabled: false,
|
||||||
|
scrollGesturesEnabled: false,
|
||||||
|
rotateGesturesEnabled: false,
|
||||||
|
myLocationEnabled: false,
|
||||||
|
attributionButtonMargins: showAttribution == false ? const Point(-100, 0) : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (assetMarkerRemoteId != null && assetThumbhash != null)
|
if (assetMarkerRemoteId != null && assetThumbhash != null)
|
||||||
WidgetLayer(
|
Container(
|
||||||
markers: [
|
width: width,
|
||||||
Marker(
|
height: height / 2,
|
||||||
point: centre,
|
|
||||||
size: Size.square(height / 2),
|
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: height / 2.5,
|
||||||
child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!),
|
child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/widgets/map/asset_marker_icon.dart';
|
||||||
|
|
||||||
|
class PositionedAssetMarkerIcon extends StatelessWidget {
|
||||||
|
final Point<num> point;
|
||||||
|
final String assetRemoteId;
|
||||||
|
final String assetThumbhash;
|
||||||
|
final double size;
|
||||||
|
final int durationInMilliseconds;
|
||||||
|
|
||||||
|
final Function()? onTap;
|
||||||
|
|
||||||
|
const PositionedAssetMarkerIcon({
|
||||||
|
required this.point,
|
||||||
|
required this.assetRemoteId,
|
||||||
|
required this.assetThumbhash,
|
||||||
|
this.size = 100,
|
||||||
|
this.durationInMilliseconds = 100,
|
||||||
|
this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio;
|
||||||
|
return AnimatedPositioned(
|
||||||
|
left: point.x / ratio - size / 2,
|
||||||
|
top: point.y / ratio - size,
|
||||||
|
duration: Duration(milliseconds: durationInMilliseconds),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onTap?.call(),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: size,
|
||||||
|
child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart';
|
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart';
|
||||||
import 'package:maplibre/maplibre.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class SearchMapThumbnail extends StatelessWidget {
|
class SearchMapThumbnail extends StatelessWidget {
|
||||||
const SearchMapThumbnail({super.key, this.size = 60.0});
|
const SearchMapThumbnail({super.key, this.size = 60.0});
|
||||||
@@ -20,13 +20,7 @@ class SearchMapThumbnail extends StatelessWidget {
|
|||||||
context.pushRoute(MapRoute());
|
context.pushRoute(MapRoute());
|
||||||
},
|
},
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false),
|
||||||
zoom: 2,
|
|
||||||
centre: const Geographic(lat: 47, lon: 5),
|
|
||||||
height: size,
|
|
||||||
width: size,
|
|
||||||
showAttribution: false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
|
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
|
||||||
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
|
subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(),
|
||||||
),
|
),
|
||||||
const BetaTimelineListTile(),
|
if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(),
|
||||||
if (Store.isBetaTimelineEnabled)
|
if (Store.isBetaTimelineEnabled)
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: readonlyModeEnabled,
|
valueNotifier: readonlyModeEnabled,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
import 'video_viewer_settings.dart';
|
import 'video_viewer_settings.dart';
|
||||||
|
|
||||||
@@ -8,7 +9,11 @@ class AssetViewerSettings extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final assetViewerSetting = [const ImageViewerQualitySetting(), const VideoViewerSettings()];
|
final assetViewerSetting = [
|
||||||
|
const ImageViewerQualitySetting(),
|
||||||
|
const ImageViewerTapToNavigateSetting(),
|
||||||
|
const VideoViewerSettings(),
|
||||||
|
];
|
||||||
|
|
||||||
return SettingsSubPageScaffold(settings: assetViewerSetting, showDivider: true);
|
return SettingsSubPageScaffold(settings: assetViewerSetting, showDivider: true);
|
||||||
}
|
}
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
|
||||||
|
class ImageViewerTapToNavigateSetting extends HookConsumerWidget {
|
||||||
|
const ImageViewerTapToNavigateSetting({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tapToNavigate = useAppSettingsState(AppSettingsEnum.tapToNavigate);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SettingsSubTitle(title: "setting_image_navigation_title".tr()),
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: tapToNavigate,
|
||||||
|
title: "setting_image_navigation_enable_title".tr(),
|
||||||
|
subtitle: "setting_image_navigation_enable_subtitle".tr(),
|
||||||
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user