mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 21:25:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a29dc703b6 |
@@ -94,7 +94,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
@@ -220,7 +219,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install Flutter dependencies
|
- name: Install Flutter dependencies
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'open-api/**'
|
- 'open-api/**'
|
||||||
- 'mobile/lib/utils/openapi_patching.dart'
|
|
||||||
- '.github/workflows/check-openapi.yml'
|
- '.github/workflows/check-openapi.yml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -30,36 +29,3 @@ jobs:
|
|||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
fail-on: ERR
|
fail-on: ERR
|
||||||
|
|
||||||
check-mobile-patches:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Mise
|
|
||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
|
||||||
with:
|
|
||||||
github_token: ${{ github.token }}
|
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Get packages
|
|
||||||
working-directory: ./mobile
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Fetch base spec from main
|
|
||||||
run: |
|
|
||||||
curl -fsSL \
|
|
||||||
"https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json" \
|
|
||||||
-o /tmp/base-spec.json
|
|
||||||
|
|
||||||
- name: Check newly-required fields have a backward-compat patch
|
|
||||||
working-directory: ./mobile
|
|
||||||
env:
|
|
||||||
OPENAPI_BASE_SPEC: /tmp/base-spec.json
|
|
||||||
OPENAPI_REVISION_SPEC: ../open-api/immich-openapi-specs.json
|
|
||||||
run: flutter test test/openapi_patches_coverage.dart
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|||||||
@@ -560,7 +560,6 @@ jobs:
|
|||||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.token.outputs.token }}
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
working_directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ services:
|
|||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
# increase readingTimeouts for the entrypoint used here
|
# increase readingTimeouts for the entrypoint used here
|
||||||
traefik.http.routers.immich.entrypoints: websecure
|
traefik.http.routers.immich.entrypoints: websecure
|
||||||
traefik.http.routers.immich.rule: Host(`immich.example.com`)
|
traefik.http.routers.immich.rule: Host(`immich.your-domain.com`)
|
||||||
traefik.http.services.immich.loadbalancer.server.port: 2283
|
traefik.http.services.immich.loadbalancer.server.port: 2283
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ immich-admin list-users
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
||||||
email: 'immich@example.com',
|
email: 'immich@example.com.com',
|
||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8888:80"
|
- "8888:80"
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: admin@example.com
|
PGADMIN_DEFAULT_EMAIL: user-name@domain-name.com
|
||||||
PGADMIN_DEFAULT_PASSWORD: strong-password
|
PGADMIN_DEFAULT_PASSWORD: strong-password
|
||||||
volumes:
|
volumes:
|
||||||
- pgadmin-data:/var/lib/pgadmin
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
|||||||
@@ -259,6 +259,17 @@ describe('/search', () => {
|
|||||||
assets: [assetHeic],
|
assets: [assetHeic],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
should: "should search city ('')",
|
||||||
|
deferred: () => ({
|
||||||
|
dto: {
|
||||||
|
city: '',
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
includeNull: true,
|
||||||
|
},
|
||||||
|
assets: [assetLast],
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
should: 'should search city (null)',
|
should: 'should search city (null)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
@@ -280,6 +291,18 @@ describe('/search', () => {
|
|||||||
assets: [assetDensity],
|
assets: [assetDensity],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
should: "should search state ('')",
|
||||||
|
deferred: () => ({
|
||||||
|
dto: {
|
||||||
|
state: '',
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
withExif: true,
|
||||||
|
includeNull: true,
|
||||||
|
},
|
||||||
|
assets: [assetLast, assetNotocactus],
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
should: 'should search state (null)',
|
should: 'should search state (null)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
@@ -301,6 +324,17 @@ describe('/search', () => {
|
|||||||
assets: [assetFalcon],
|
assets: [assetFalcon],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
should: "should search country ('')",
|
||||||
|
deferred: () => ({
|
||||||
|
dto: {
|
||||||
|
country: '',
|
||||||
|
visibility: AssetVisibility.Timeline,
|
||||||
|
includeNull: true,
|
||||||
|
},
|
||||||
|
assets: [assetLast],
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
should: 'should search country (null)',
|
should: 'should search country (null)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
|
|||||||
@@ -699,7 +699,6 @@
|
|||||||
"backup_settings_subtitle": "Manage upload settings",
|
"backup_settings_subtitle": "Manage upload settings",
|
||||||
"backup_upload_details_page_more_details": "Tap for more details",
|
"backup_upload_details_page_more_details": "Tap for more details",
|
||||||
"backward": "Backward",
|
"backward": "Backward",
|
||||||
"battery_optimization_backup_reliability": "Disabling battery optimizations can improve the reliability of background backup",
|
|
||||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||||
"biometric_no_options": "No biometric options available",
|
"biometric_no_options": "No biometric options available",
|
||||||
@@ -1688,10 +1687,8 @@
|
|||||||
"not_available": "N/A",
|
"not_available": "N/A",
|
||||||
"not_in_any_album": "Not in any album",
|
"not_in_any_album": "Not in any album",
|
||||||
"not_selected": "Not selected",
|
"not_selected": "Not selected",
|
||||||
"not_set": "Not set",
|
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"nothing_here_yet": "Nothing here yet",
|
"nothing_here_yet": "Nothing here yet",
|
||||||
"notification_backup_reliability": "Enable notifications to improve background backup reliability",
|
|
||||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||||
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
||||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||||
|
|||||||
@@ -816,10 +816,6 @@ class TestFaceRecognition:
|
|||||||
|
|
||||||
def test_recognition(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
|
def test_recognition(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(FaceRecognizer, "load")
|
mocker.patch.object(FaceRecognizer, "load")
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
|
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
|
||||||
|
|
||||||
num_faces = 2
|
num_faces = 2
|
||||||
@@ -864,10 +860,6 @@ class TestFaceRecognition:
|
|||||||
)
|
)
|
||||||
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
||||||
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
ort_session.return_value.get_inputs.return_value = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
|
ort_session.return_value.get_inputs.return_value = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
|
||||||
ort_session.return_value.get_outputs.return_value = [SimpleNamespace(name="output.1", shape=(1, 800))]
|
ort_session.return_value.get_outputs.return_value = [SimpleNamespace(name="output.1", shape=(1, 800))]
|
||||||
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
||||||
@@ -902,10 +894,6 @@ class TestFaceRecognition:
|
|||||||
)
|
)
|
||||||
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
||||||
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
||||||
|
|
||||||
inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
|
inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
|
||||||
@@ -1008,10 +996,6 @@ class TestFaceRecognition:
|
|||||||
|
|
||||||
def test_ignore_other_custom_max_batch_size(self, mocker: MockerFixture) -> None:
|
def test_ignore_other_custom_max_batch_size(self, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(ocr=2))
|
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(ocr=2))
|
||||||
mocker.patch(
|
|
||||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
|
||||||
return_value=["CPUExecutionProvider"],
|
|
||||||
)
|
|
||||||
|
|
||||||
recognizer = FaceRecognizer("buffalo_l", cache_dir="test_cache")
|
recognizer = FaceRecognizer("buffalo_l", cache_dir="test_cache")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,74 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
|
[[tools."aqua:flutter/flutter"]]
|
||||||
|
version = "3.44.0"
|
||||||
|
backend = "aqua:flutter/flutter"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
||||||
|
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip"
|
||||||
|
|
||||||
|
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
||||||
|
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip"
|
||||||
|
|
||||||
|
[[tools.flutter]]
|
||||||
|
version = "3.41.9-stable"
|
||||||
|
backend = "asdf:flutter"
|
||||||
|
|
||||||
|
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||||
|
version = "1.37.0"
|
||||||
|
backend = "github:CQLabs/homebrew-dcm"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
|
||||||
|
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
|
||||||
|
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
|
||||||
|
|
||||||
[[tools."github:extism/cli"]]
|
[[tools."github:extism/cli"]]
|
||||||
version = "1.6.3"
|
version = "1.6.3"
|
||||||
backend = "github:extism/cli"
|
backend = "github:extism/cli"
|
||||||
@@ -156,6 +225,30 @@ checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2
|
|||||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
|
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
|
||||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
|
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
|
||||||
|
|
||||||
|
[[tools.java]]
|
||||||
|
version = "21.0.2"
|
||||||
|
backend = "core:java"
|
||||||
|
|
||||||
|
[tools.java."platforms.linux-arm64"]
|
||||||
|
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
|
||||||
|
|
||||||
|
[tools.java."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||||
|
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||||
|
|
||||||
[[tools.node]]
|
[[tools.node]]
|
||||||
version = "24.15.0"
|
version = "24.15.0"
|
||||||
backend = "core:node"
|
backend = "core:node"
|
||||||
@@ -228,34 +321,6 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
|
|||||||
version = "10.33.4"
|
version = "10.33.4"
|
||||||
backend = "aqua:pnpm/pnpm"
|
backend = "aqua:pnpm/pnpm"
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64"]
|
|
||||||
checksum = "sha256:d29649c7380b5cd522f574208fbd35335846686498f45004604d3f5b8658b5cb"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-arm64"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
|
||||||
checksum = "sha256:d29649c7380b5cd522f574208fbd35335846686498f45004604d3f5b8658b5cb"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-arm64"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64"]
|
|
||||||
checksum = "sha256:ff1795595535a10d0dfe327303f3dd02377be141190b1f5756de68edde2cf813"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-x64"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.linux-x64-musl"]
|
|
||||||
checksum = "sha256:ff1795595535a10d0dfe327303f3dd02377be141190b1f5756de68edde2cf813"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-x64"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.macos-arm64"]
|
|
||||||
checksum = "sha256:7aae186a04e1ffaa0047d43cd07d68a98dec303304f28be52234ba955d26c671"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-macos-arm64"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.macos-x64"]
|
|
||||||
checksum = "sha256:3b0c97b9f794cdda293949a8ee0e0151ca08f512f4a832408386221c7c73eec6"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-macos-x64"
|
|
||||||
|
|
||||||
[tools.pnpm."platforms.windows-x64"]
|
|
||||||
checksum = "sha256:3268b2f29defe0dce8a3a26c0ef01488f0d4aa4872923173186ef618ab7d68ef"
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-win-x64.exe"
|
|
||||||
|
|
||||||
[[tools.terragrunt]]
|
[[tools.terragrunt]]
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
backend = "aqua:gruntwork-io/terragrunt"
|
backend = "aqua:gruntwork-io/terragrunt"
|
||||||
|
|||||||
@@ -16,14 +16,28 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
|
"aqua:flutter/flutter" = "3.44.0"
|
||||||
pnpm = "10.33.4"
|
pnpm = "10.33.4"
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
|
java = "21.0.2"
|
||||||
"npm:oazapfts" = "7.5.0"
|
"npm:oazapfts" = "7.5.0"
|
||||||
"github:extism/cli" = "1.6.3"
|
"github:extism/cli" = "1.6.3"
|
||||||
"github:webassembly/binaryen" = "version_124"
|
"github:webassembly/binaryen" = "version_124"
|
||||||
"github:extism/js-pdk" = "1.6.0"
|
"github:extism/js-pdk" = "1.6.0"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
|
version = "1.37.0"
|
||||||
|
bin = "dcm"
|
||||||
|
postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
|
||||||
|
|
||||||
|
[tools."github:CQLabs/homebrew-dcm".platforms]
|
||||||
|
linux-x64 = { asset_pattern = "dcm-linux-x64-release.zip" }
|
||||||
|
linux-arm64 = { asset_pattern = "dcm-linux-arm-release.zip" }
|
||||||
|
macos-x64 = { asset_pattern = "dcm-macos-x64-release.zip" }
|
||||||
|
macos-arm64 = { asset_pattern = "dcm-macos-arm-release.zip" }
|
||||||
|
windows-x64 = { asset_pattern = "dcm-windows-release.zip" }
|
||||||
|
|
||||||
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
[tools."github:jellyfin/jellyfin-ffmpeg"]
|
||||||
version = "7.1.3-6"
|
version = "7.1.3-6"
|
||||||
|
|
||||||
|
|||||||
@@ -89,20 +89,6 @@
|
|||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Allow Immich to act as an image viewer -->
|
|
||||||
<intent-filter android:label="View in Immich">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="content" android:mimeType="image/*" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Allow Immich to act as a video viewer -->
|
|
||||||
<intent-filter android:label="View in Immich">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="content" android:mimeType="video/*" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- immich:// URL scheme handling -->
|
<!-- immich:// URL scheme handling -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
import app.alextran.immich.background.BackgroundEngineLock
|
import app.alextran.immich.background.BackgroundEngineLock
|
||||||
@@ -23,7 +22,6 @@ import app.alextran.immich.permission.PermissionApiImpl
|
|||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl26
|
import app.alextran.immich.sync.NativeSyncApiImpl26
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl30
|
import app.alextran.immich.sync.NativeSyncApiImpl30
|
||||||
import app.alextran.immich.viewintent.ViewIntentPlugin
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
@@ -33,11 +31,6 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
registerPlugins(this, flutterEngine)
|
registerPlugins(this, flutterEngine)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
setIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) {
|
||||||
HttpClientManager.initialize(ctx)
|
HttpClientManager.initialize(ctx)
|
||||||
@@ -62,7 +55,6 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
|
|
||||||
flutterEngine.plugins.add(ViewIntentPlugin())
|
|
||||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||||
flutterEngine.plugins.add(permissionApiImpl)
|
flutterEngine.plugins.add(permissionApiImpl)
|
||||||
|
|||||||
+2
-43
@@ -47,44 +47,18 @@ class FlutterError (
|
|||||||
override val message: String? = null,
|
override val message: String? = null,
|
||||||
val details: Any? = null
|
val details: Any? = null
|
||||||
) : RuntimeException()
|
) : RuntimeException()
|
||||||
|
|
||||||
enum class PermissionStatus(val raw: Int) {
|
|
||||||
GRANTED(0),
|
|
||||||
DENIED(1),
|
|
||||||
PERMANENTLY_DENIED(2);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun ofRaw(raw: Int): PermissionStatus? {
|
|
||||||
return values().firstOrNull { it.raw == raw }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
|
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
return when (type) {
|
return super.readValueOfType(type, buffer)
|
||||||
129.toByte() -> {
|
|
||||||
return (readValue(buffer) as Long?)?.let {
|
|
||||||
PermissionStatus.ofRaw(it.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.readValueOfType(type, buffer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
when (value) {
|
super.writeValue(stream, value)
|
||||||
is PermissionStatus -> {
|
|
||||||
stream.write(129)
|
|
||||||
writeValue(stream, value.raw.toLong())
|
|
||||||
}
|
|
||||||
else -> super.writeValue(stream, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface PermissionApi {
|
interface PermissionApi {
|
||||||
fun isIgnoringBatteryOptimizations(): PermissionStatus
|
|
||||||
fun hasManageMediaPermission(): Boolean
|
fun hasManageMediaPermission(): Boolean
|
||||||
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
@@ -98,21 +72,6 @@ interface PermissionApi {
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
listOf(api.isIgnoringBatteryOptimizations())
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
PermissionApiPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
-13
@@ -1,26 +1,13 @@
|
|||||||
package app.alextran.immich.permission
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.PowerManager
|
|
||||||
import app.alextran.immich.core.ImmichPlugin
|
import app.alextran.immich.core.ImmichPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
|
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
|
||||||
private val ctx: Context = context.applicationContext
|
|
||||||
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
|
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
|
||||||
|
|
||||||
private val powerManager =
|
|
||||||
ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
||||||
|
|
||||||
|
|
||||||
override fun isIgnoringBatteryOptimizations(): PermissionStatus {
|
|
||||||
if (powerManager.isIgnoringBatteryOptimizations(ctx.packageName)) {
|
|
||||||
return PermissionStatus.GRANTED
|
|
||||||
}
|
|
||||||
return PermissionStatus.DENIED
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasManageMediaPermission(): Boolean =
|
override fun hasManageMediaPermission(): Boolean =
|
||||||
manageMediaPermissionDelegate.hasManageMediaPermission()
|
manageMediaPermissionDelegate.hasManageMediaPermission()
|
||||||
|
|
||||||
|
|||||||
+34
-66
@@ -542,17 +542,16 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface NativeSyncApi {
|
interface NativeSyncApi {
|
||||||
fun shouldFullSync(callback: (Result<Boolean>) -> Unit)
|
fun shouldFullSync(): Boolean
|
||||||
fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit)
|
fun getMediaChanges(): SyncDelta
|
||||||
fun checkpointSync()
|
fun checkpointSync()
|
||||||
fun clearSyncCheckpoint()
|
fun clearSyncCheckpoint()
|
||||||
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit)
|
fun getAssetIdsForAlbum(albumId: String): List<String>
|
||||||
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit)
|
fun getAlbums(): List<PlatformAlbum>
|
||||||
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
||||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit)
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
|
||||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||||
fun cancelHashing()
|
fun cancelHashing()
|
||||||
fun cancelSync()
|
|
||||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
||||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||||
@@ -571,33 +570,27 @@ interface NativeSyncApi {
|
|||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
api.shouldFullSync{ result: Result<Boolean> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.shouldFullSync())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
api.getMediaChanges{ result: Result<SyncDelta> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getMediaChanges())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -636,38 +629,32 @@ interface NativeSyncApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { message, reply ->
|
channel.setMessageHandler { message, reply ->
|
||||||
val args = message as List<Any?>
|
val args = message as List<Any?>
|
||||||
val albumIdArg = args[0] as String
|
val albumIdArg = args[0] as String
|
||||||
api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetIdsForAlbum(albumIdArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
api.getAlbums{ result: Result<List<PlatformAlbum>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAlbums())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -692,21 +679,18 @@ interface NativeSyncApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { message, reply ->
|
channel.setMessageHandler { message, reply ->
|
||||||
val args = message as List<Any?>
|
val args = message as List<Any?>
|
||||||
val albumIdArg = args[0] as String
|
val albumIdArg = args[0] as String
|
||||||
val updatedTimeCondArg = args[1] as Long?
|
val updatedTimeCondArg = args[1] as Long?
|
||||||
api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result<List<PlatformAsset>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -749,22 +733,6 @@ interface NativeSyncApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
val wrapped: List<Any?> = try {
|
|
||||||
api.cancelSync()
|
|
||||||
listOf(null)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
MessagesPigeonUtils.wrapError(exception)
|
|
||||||
}
|
|
||||||
reply.reply(wrapped)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import android.content.Context
|
|||||||
|
|
||||||
|
|
||||||
class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
|
class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
|
||||||
override fun shouldFullSync(callback: (Result<Boolean>) -> Unit) {
|
override fun shouldFullSync(): Boolean {
|
||||||
runSync(callback) { shouldFullSync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldFullSync(): Boolean {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +18,7 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
// No-op for Android 10 and below
|
// No-op for Android 10 and below
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) {
|
override fun getMediaChanges(): SyncDelta {
|
||||||
runSync(callback) { getMediaChanges() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMediaChanges(): SyncDelta {
|
|
||||||
throw IllegalStateException("Method not supported on this Android version.")
|
throw IllegalStateException("Method not supported on this Android version.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import android.os.Bundle
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.RequiresExtension
|
import androidx.annotation.RequiresExtension
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@@ -37,11 +35,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldFullSync(callback: (Result<Boolean>) -> Unit) {
|
override fun shouldFullSync(): Boolean =
|
||||||
runSync(callback) { shouldFullSync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldFullSync(): Boolean =
|
|
||||||
MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
|
MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
|
||||||
|
|
||||||
override fun checkpointSync() {
|
override fun checkpointSync() {
|
||||||
@@ -55,11 +49,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) {
|
override fun getMediaChanges(): SyncDelta {
|
||||||
runSync(callback) { getMediaChanges() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getMediaChanges(): SyncDelta {
|
|
||||||
val genMap = getSavedGenerationMap()
|
val genMap = getSavedGenerationMap()
|
||||||
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
||||||
val changed = mutableListOf<PlatformAsset>()
|
val changed = mutableListOf<PlatformAsset>()
|
||||||
@@ -68,7 +58,6 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
var hasChanges = genMap.keys != currentVolumes
|
var hasChanges = genMap.keys != currentVolumes
|
||||||
|
|
||||||
for (volume in currentVolumes) {
|
for (volume in currentVolumes) {
|
||||||
currentCoroutineContext().ensureActive()
|
|
||||||
val currentGen = MediaStore.getGeneration(ctx, volume)
|
val currentGen = MediaStore.getGeneration(ctx, volume)
|
||||||
val storedGen = genMap[volume] ?: 0
|
val storedGen = genMap[volume] ?: 0
|
||||||
if (currentGen <= storedGen) {
|
if (currentGen <= storedGen) {
|
||||||
|
|||||||
@@ -45,14 +45,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
private var hashTask: Job? = null
|
private var hashTask: Job? = null
|
||||||
private var syncJob: Job? = null
|
|
||||||
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
||||||
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS)
|
||||||
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED"
|
||||||
private const val SYNC_CANCELLED_CODE = "SYNC_CANCELLED"
|
|
||||||
|
|
||||||
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
// MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+
|
||||||
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
|
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
|
||||||
@@ -297,11 +295,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
return PlatformAssetPlaybackStyle.IMAGE
|
return PlatformAssetPlaybackStyle.IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit) {
|
fun getAlbums(): List<PlatformAlbum> {
|
||||||
runSync(callback) { getAlbums() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getAlbums(): List<PlatformAlbum> {
|
|
||||||
val albums = mutableListOf<PlatformAlbum>()
|
val albums = mutableListOf<PlatformAlbum>()
|
||||||
val albumsCount = mutableMapOf<String, Int>()
|
val albumsCount = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
@@ -328,7 +322,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
currentCoroutineContext().ensureActive()
|
|
||||||
val id = cursor.getString(bucketIdColumn)
|
val id = cursor.getString(bucketIdColumn)
|
||||||
|
|
||||||
val count = albumsCount.getOrDefault(id, 0)
|
val count = albumsCount.getOrDefault(id, 0)
|
||||||
@@ -349,11 +342,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
.sortedBy { it.id }
|
.sortedBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) {
|
fun getAssetIdsForAlbum(albumId: String): List<String> {
|
||||||
runSync(callback) { getAssetIdsForAlbum(albumId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAssetIdsForAlbum(albumId: String): List<String> {
|
|
||||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
|
|
||||||
return getCursor(
|
return getCursor(
|
||||||
@@ -377,11 +366,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
||||||
|
|
||||||
|
|
||||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit) {
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
|
||||||
runSync(callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
|
|
||||||
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
|
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
|
||||||
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
|
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
|
||||||
|
|
||||||
@@ -466,24 +451,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||||||
hashTask = null
|
hashTask = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelSync() {
|
|
||||||
syncJob?.cancel()
|
|
||||||
syncJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun <T> runSync(callback: (Result<T>) -> Unit, work: suspend () -> T) {
|
|
||||||
syncJob?.cancel()
|
|
||||||
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
completeWhenActive(callback, Result.success(work()))
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
completeWhenActive(callback, Result.failure(FlutterError(SYNC_CANCELLED_CODE, "Sync cancelled", null)))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
completeWhenActive(callback, Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
||||||
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
||||||
}
|
}
|
||||||
|
|||||||
-292
@@ -1,292 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
|
||||||
|
|
||||||
package app.alextran.immich.viewintent
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.flutter.plugin.common.BasicMessageChannel
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
|
||||||
import io.flutter.plugin.common.EventChannel
|
|
||||||
import io.flutter.plugin.common.MessageCodec
|
|
||||||
import io.flutter.plugin.common.StandardMethodCodec
|
|
||||||
import io.flutter.plugin.common.StandardMessageCodec
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
private object ViewIntentPigeonUtils {
|
|
||||||
|
|
||||||
fun wrapResult(result: Any?): List<Any?> {
|
|
||||||
return listOf(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wrapError(exception: Throwable): List<Any?> {
|
|
||||||
return if (exception is FlutterError) {
|
|
||||||
listOf(
|
|
||||||
exception.code,
|
|
||||||
exception.message,
|
|
||||||
exception.details
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
exception.javaClass.simpleName,
|
|
||||||
exception.toString(),
|
|
||||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
|
||||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun floatEquals(a: Float, b: Float): Boolean {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
|
||||||
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doubleHash(d: Double): Int {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
|
||||||
val normalized = if (d == 0.0) 0.0 else d
|
|
||||||
val bits = java.lang.Double.doubleToLongBits(normalized)
|
|
||||||
return (bits xor (bits ushr 32)).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun floatHash(f: Float): Int {
|
|
||||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
|
||||||
val normalized = if (f == 0.0f) 0.0f else f
|
|
||||||
return java.lang.Float.floatToIntBits(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
|
||||||
if (a === b) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a == null || b == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (a is ByteArray && b is ByteArray) {
|
|
||||||
return a.contentEquals(b)
|
|
||||||
}
|
|
||||||
if (a is IntArray && b is IntArray) {
|
|
||||||
return a.contentEquals(b)
|
|
||||||
}
|
|
||||||
if (a is LongArray && b is LongArray) {
|
|
||||||
return a.contentEquals(b)
|
|
||||||
}
|
|
||||||
if (a is DoubleArray && b is DoubleArray) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (i in a.indices) {
|
|
||||||
if (!doubleEquals(a[i], b[i])) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is FloatArray && b is FloatArray) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (i in a.indices) {
|
|
||||||
if (!floatEquals(a[i], b[i])) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is Array<*> && b is Array<*>) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (i in a.indices) {
|
|
||||||
if (!deepEquals(a[i], b[i])) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is List<*> && b is List<*>) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
val iterA = a.iterator()
|
|
||||||
val iterB = b.iterator()
|
|
||||||
while (iterA.hasNext() && iterB.hasNext()) {
|
|
||||||
if (!deepEquals(iterA.next(), iterB.next())) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is Map<*, *> && b is Map<*, *>) {
|
|
||||||
if (a.size != b.size) return false
|
|
||||||
for (entry in a) {
|
|
||||||
val key = entry.key
|
|
||||||
var found = false
|
|
||||||
for (bEntry in b) {
|
|
||||||
if (deepEquals(key, bEntry.key)) {
|
|
||||||
if (deepEquals(entry.value, bEntry.value)) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (a is Double && b is Double) {
|
|
||||||
return doubleEquals(a, b)
|
|
||||||
}
|
|
||||||
if (a is Float && b is Float) {
|
|
||||||
return floatEquals(a, b)
|
|
||||||
}
|
|
||||||
return a == b
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deepHash(value: Any?): Int {
|
|
||||||
return when (value) {
|
|
||||||
null -> 0
|
|
||||||
is ByteArray -> value.contentHashCode()
|
|
||||||
is IntArray -> value.contentHashCode()
|
|
||||||
is LongArray -> value.contentHashCode()
|
|
||||||
is DoubleArray -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + doubleHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is FloatArray -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + floatHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is Array<*> -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + deepHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is List<*> -> {
|
|
||||||
var result = 1
|
|
||||||
for (item in value) {
|
|
||||||
result = 31 * result + deepHash(item)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is Map<*, *> -> {
|
|
||||||
var result = 0
|
|
||||||
for (entry in value) {
|
|
||||||
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
is Double -> doubleHash(value)
|
|
||||||
is Float -> floatHash(value)
|
|
||||||
else -> value.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
|
||||||
* @property code The error code.
|
|
||||||
* @property message The error message.
|
|
||||||
* @property details The error details. Must be a datatype supported by the api codec.
|
|
||||||
*/
|
|
||||||
class FlutterError (
|
|
||||||
val code: String,
|
|
||||||
override val message: String? = null,
|
|
||||||
val details: Any? = null
|
|
||||||
) : RuntimeException()
|
|
||||||
|
|
||||||
/** Generated class from Pigeon that represents data sent in messages. */
|
|
||||||
data class ViewIntentPayload (
|
|
||||||
val path: String? = null,
|
|
||||||
val mimeType: String,
|
|
||||||
val localAssetId: String? = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
companion object {
|
|
||||||
fun fromList(pigeonVar_list: List<Any?>): ViewIntentPayload {
|
|
||||||
val path = pigeonVar_list[0] as String?
|
|
||||||
val mimeType = pigeonVar_list[1] as String
|
|
||||||
val localAssetId = pigeonVar_list[2] as String?
|
|
||||||
return ViewIntentPayload(path, mimeType, localAssetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun toList(): List<Any?> {
|
|
||||||
return listOf(
|
|
||||||
path,
|
|
||||||
mimeType,
|
|
||||||
localAssetId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other == null || other.javaClass != javaClass) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this === other) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val other = other as ViewIntentPayload
|
|
||||||
return ViewIntentPigeonUtils.deepEquals(this.path, other.path) && ViewIntentPigeonUtils.deepEquals(this.mimeType, other.mimeType) && ViewIntentPigeonUtils.deepEquals(this.localAssetId, other.localAssetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = javaClass.hashCode()
|
|
||||||
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.path)
|
|
||||||
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.mimeType)
|
|
||||||
result = 31 * result + ViewIntentPigeonUtils.deepHash(this.localAssetId)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private open class ViewIntentPigeonCodec : StandardMessageCodec() {
|
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
|
||||||
return when (type) {
|
|
||||||
129.toByte() -> {
|
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
|
||||||
ViewIntentPayload.fromList(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.readValueOfType(type, buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
|
||||||
when (value) {
|
|
||||||
is ViewIntentPayload -> {
|
|
||||||
stream.write(129)
|
|
||||||
writeValue(stream, value.toList())
|
|
||||||
}
|
|
||||||
else -> super.writeValue(stream, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
|
||||||
interface ViewIntentHostApi {
|
|
||||||
fun consumeViewIntent(callback: (Result<ViewIntentPayload?>) -> Unit)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** The codec used by ViewIntentHostApi. */
|
|
||||||
val codec: MessageCodec<Any?> by lazy {
|
|
||||||
ViewIntentPigeonCodec()
|
|
||||||
}
|
|
||||||
/** Sets up an instance of `ViewIntentHostApi` to handle messages through the `binaryMessenger`. */
|
|
||||||
@JvmOverloads
|
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: ViewIntentHostApi?, messageChannelSuffix: String = "") {
|
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
|
||||||
run {
|
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$separatedMessageChannelSuffix", codec)
|
|
||||||
if (api != null) {
|
|
||||||
channel.setMessageHandler { _, reply ->
|
|
||||||
api.consumeViewIntent{ result: Result<ViewIntentPayload?> ->
|
|
||||||
val error = result.exceptionOrNull()
|
|
||||||
if (error != null) {
|
|
||||||
reply.reply(ViewIntentPigeonUtils.wrapError(error))
|
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(ViewIntentPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
channel.setMessageHandler(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
package app.alextran.immich.viewintent
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
|
||||||
import io.flutter.plugin.common.PluginRegistry
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
private const val TAG = "ViewIntentPlugin"
|
|
||||||
|
|
||||||
class ViewIntentPlugin : FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener, ViewIntentHostApi {
|
|
||||||
private var context: Context? = null
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var unconsumedIntent: Intent? = null
|
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
context = binding.applicationContext
|
|
||||||
ViewIntentHostApi.setUp(binding.binaryMessenger, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
ViewIntentHostApi.setUp(binding.binaryMessenger, null)
|
|
||||||
ioScope.cancel()
|
|
||||||
context = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
|
||||||
activity = binding.activity
|
|
||||||
unconsumedIntent = binding.activity.intent
|
|
||||||
binding.addOnNewIntentListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromActivityForConfigChanges() {
|
|
||||||
activity = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
|
||||||
onAttachedToActivity(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {
|
|
||||||
activity = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent): Boolean {
|
|
||||||
unconsumedIntent = intent
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun consumeViewIntent(callback: (Result<ViewIntentPayload?>) -> Unit) {
|
|
||||||
val context = context ?: run {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val intent = unconsumedIntent ?: activity?.intent
|
|
||||||
|
|
||||||
if (intent?.action != Intent.ACTION_VIEW) {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = intent.data
|
|
||||||
if (uri == null) {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ioScope.launch {
|
|
||||||
try {
|
|
||||||
val mimeType = context.contentResolver.getType(uri) ?: intent.type
|
|
||||||
if (mimeType == null || (!mimeType.startsWith("image/") && !mimeType.startsWith("video/"))) {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val localAssetId = extractLocalAssetId(context, uri, mimeType)
|
|
||||||
val tempFilePath = if (localAssetId == null) {
|
|
||||||
copyUriToTempFile(context, uri, mimeType)?.absolutePath ?: run {
|
|
||||||
callback(Result.success(null))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val payload = ViewIntentPayload(
|
|
||||||
path = tempFilePath,
|
|
||||||
mimeType = mimeType,
|
|
||||||
localAssetId = localAssetId,
|
|
||||||
)
|
|
||||||
consumeViewIntent(intent)
|
|
||||||
callback(Result.success(payload))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
callback(Result.failure(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun consumeViewIntent(currentIntent: Intent) {
|
|
||||||
unconsumedIntent = Intent(currentIntent).apply {
|
|
||||||
action = null
|
|
||||||
data = null
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
activity?.intent = unconsumedIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractLocalAssetId(context: Context, uri: Uri, mimeType: String): String? {
|
|
||||||
return tryExtractDocumentLocalAssetId(context, uri)
|
|
||||||
?: tryParseContentUriId(uri)
|
|
||||||
?: resolveLocalIdByNameAndSize(context, uri, mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryExtractDocumentLocalAssetId(context: Context, uri: Uri): String? {
|
|
||||||
return try {
|
|
||||||
if (!DocumentsContract.isDocumentUri(context, uri)) return null
|
|
||||||
val docId = DocumentsContract.getDocumentId(uri)
|
|
||||||
if (docId.isBlank() || docId.startsWith("raw:")) return null
|
|
||||||
docId.substringAfter(':', docId).toLongOrNull()?.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Failed to resolve local asset id from document URI: $uri", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryParseContentUriId(uri: Uri): String? {
|
|
||||||
val id = uri.lastPathSegment?.toLongOrNull() ?: return null
|
|
||||||
return if (id >= 0) id.toString() else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyUriToTempFile(context: Context, uri: Uri, mimeType: String): File? {
|
|
||||||
return try {
|
|
||||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
|
|
||||||
val tempFile = File.createTempFile("view_intent_", extension, context.cacheDir)
|
|
||||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
|
||||||
FileOutputStream(tempFile).use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
} ?: return null
|
|
||||||
tempFile
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveLocalIdByNameAndSize(context: Context, uri: Uri, mimeType: String): String? {
|
|
||||||
val metaProjection = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
|
||||||
val (displayName, size) =
|
|
||||||
try {
|
|
||||||
context.contentResolver.query(uri, metaProjection, null, null, null)?.use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) return null
|
|
||||||
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
||||||
val name = if (nameIdx >= 0) cursor.getString(nameIdx) else null
|
|
||||||
val bytes = if (sizeIdx >= 0) cursor.getLong(sizeIdx) else -1L
|
|
||||||
if (name.isNullOrBlank() || bytes < 0) return null
|
|
||||||
name to bytes
|
|
||||||
} ?: return null
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val tableUri = when {
|
|
||||||
mimeType.startsWith("image/") -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
||||||
mimeType.startsWith("video/") -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
context.contentResolver
|
|
||||||
.query(
|
|
||||||
tableUri,
|
|
||||||
arrayOf(MediaStore.MediaColumns._ID),
|
|
||||||
"${MediaStore.MediaColumns.DISPLAY_NAME}=? AND ${MediaStore.MediaColumns.SIZE}=?",
|
|
||||||
arrayOf(displayName, size.toString()),
|
|
||||||
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC",
|
|
||||||
)?.use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) return null
|
|
||||||
val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
|
|
||||||
if (idIndex < 0) return null
|
|
||||||
cursor.getLong(idIndex).toString()
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-3391
File diff suppressed because it is too large
Load Diff
@@ -1,154 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|
||||||
import 'package:immich_mobile/main.dart' as app;
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
|
||||||
import 'package:immich_mobile/wm_executor.dart';
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
import 'test_utils/fake_immich_server.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
// These tests do real I/O without pumping a widget tree, so disable the fake async clock
|
|
||||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
|
||||||
|
|
||||||
late Drift drift;
|
|
||||||
late FakeImmichServer server;
|
|
||||||
|
|
||||||
setUpAll(() async {
|
|
||||||
await app.initApp();
|
|
||||||
(drift, _) = await Bootstrap.initDomain();
|
|
||||||
});
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
await workerManagerPatch.init(dynamicSpawning: true);
|
|
||||||
server = await FakeImmichServer.start();
|
|
||||||
await ApiService().resolveAndSetEndpoint(server.endpoint);
|
|
||||||
await drift.delete(drift.userEntity).go();
|
|
||||||
await Store.delete(StoreKey.syncMigrationStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
await workerManagerPatch.dispose();
|
|
||||||
await server.close();
|
|
||||||
await Store.delete(StoreKey.serverEndpoint);
|
|
||||||
await Store.delete(StoreKey.syncMigrationStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
void sendUser(SyncStream stream, String id, String name) {
|
|
||||||
stream.send(
|
|
||||||
type: SyncEntityType.userV1.value,
|
|
||||||
data: SyncUserV1(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
email: '$id@test.com',
|
|
||||||
hasProfileImage: false,
|
|
||||||
deletedAt: null,
|
|
||||||
profileChangedAt: DateTime.utc(2025),
|
|
||||||
).toJson(),
|
|
||||||
ack: id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> dbReadable() async {
|
|
||||||
try {
|
|
||||||
await drift.customSelect('SELECT 1').get().timeout(const Duration(seconds: 5));
|
|
||||||
return true;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> userCount() async => (await drift.select(drift.userEntity).get()).length;
|
|
||||||
|
|
||||||
// Starts a remote sync and resolves once its /sync/stream request is open.
|
|
||||||
Future<(Future<bool>, SyncStream)> startSync() async {
|
|
||||||
final sync = BackgroundSyncManager().syncRemote();
|
|
||||||
final stream = await server.streamOpened.timeout(
|
|
||||||
const Duration(seconds: 30),
|
|
||||||
onTimeout: () => fail('sync isolate never opened /sync/stream'),
|
|
||||||
);
|
|
||||||
return (sync, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets('a full sync ingests streamed events into the shared DB', (tester) async {
|
|
||||||
expect(await userCount(), 0);
|
|
||||||
|
|
||||||
final (sync, stream) = await startSync();
|
|
||||||
|
|
||||||
sendUser(stream, 'u1', 'Alice');
|
|
||||||
sendUser(stream, 'u2', 'Bob');
|
|
||||||
await stream.close();
|
|
||||||
|
|
||||||
final result = await sync.timeout(
|
|
||||||
const Duration(seconds: 30),
|
|
||||||
onTimeout: () => fail('sync did not complete after the stream ended'),
|
|
||||||
);
|
|
||||||
expect(result, isTrue);
|
|
||||||
expect(await userCount(), 2);
|
|
||||||
expect(server.ackRequests, greaterThan(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('disposing the pool during an in-flight sync drains promptly', (tester) async {
|
|
||||||
final (sync, _) = await startSync();
|
|
||||||
|
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
await workerManagerPatch.dispose().timeout(
|
|
||||||
const Duration(seconds: 15),
|
|
||||||
onTimeout: () => fail('dispose() hung — worker did not drain and exit'),
|
|
||||||
);
|
|
||||||
expect(sw.elapsed, lessThan(const Duration(seconds: 10)), reason: 'abort-driven, not socket-timeout bound');
|
|
||||||
|
|
||||||
expect(await sync.timeout(const Duration(seconds: 5), onTimeout: () => false), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('tearing down a worker blocked mid-write leaves the DB usable', (tester) async {
|
|
||||||
final (sync, stream) = await startSync();
|
|
||||||
|
|
||||||
// Hold an exclusive write transaction so the worker's write is blocked. The lock is taken only
|
|
||||||
// after the stream opens to avoid blocking the worker's own startup DB reads.
|
|
||||||
final releaseTxn = Completer<void>();
|
|
||||||
final txnHeld = Completer<void>();
|
|
||||||
final txn = drift.transaction(() async {
|
|
||||||
await drift.into(drift.userEntity).insert(
|
|
||||||
UserEntityCompanion.insert(
|
|
||||||
id: 'holder',
|
|
||||||
name: 'holder',
|
|
||||||
email: 'holder@test.com',
|
|
||||||
hasProfileImage: const Value(false),
|
|
||||||
profileChangedAt: Value(DateTime.utc(2025)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
txnHeld.complete();
|
|
||||||
await releaseTxn.future;
|
|
||||||
});
|
|
||||||
await txnHeld.future;
|
|
||||||
|
|
||||||
sendUser(stream, 'u1', 'Alice');
|
|
||||||
await stream.close();
|
|
||||||
|
|
||||||
// dispose() can only finish once the worker unwinds, which is blocked on the
|
|
||||||
// lock — so start it, release the lock, then await completion.
|
|
||||||
final disposed = workerManagerPatch.dispose();
|
|
||||||
releaseTxn.complete();
|
|
||||||
await txn;
|
|
||||||
await disposed.timeout(
|
|
||||||
const Duration(seconds: 15),
|
|
||||||
onTimeout: () => fail('dispose() hung after releasing the write lock'),
|
|
||||||
);
|
|
||||||
await sync.timeout(const Duration(seconds: 5), onTimeout: () => false);
|
|
||||||
|
|
||||||
expect(await dbReadable(), isTrue);
|
|
||||||
final users = await drift.select(drift.userEntity).get();
|
|
||||||
expect(users.map((u) => u.id), contains('holder'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
/// A dummy localhost server that implements only the endpoints that remote-sync touches.
|
|
||||||
class FakeImmichServer {
|
|
||||||
FakeImmichServer._(this._server, this.version);
|
|
||||||
|
|
||||||
final HttpServer _server;
|
|
||||||
final (int, int, int) version;
|
|
||||||
|
|
||||||
final Completer<SyncStream> _streamOpened = Completer<SyncStream>();
|
|
||||||
|
|
||||||
int ackRequests = 0;
|
|
||||||
|
|
||||||
String get endpoint => 'http://${_server.address.host}:${_server.port}/api';
|
|
||||||
|
|
||||||
/// Resolves when the sync isolate opens `POST /sync/stream`.
|
|
||||||
Future<SyncStream> get streamOpened => _streamOpened.future;
|
|
||||||
|
|
||||||
static Future<FakeImmichServer> start({(int, int, int) version = (3, 0, 0)}) async {
|
|
||||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
||||||
final fake = FakeImmichServer._(server, version);
|
|
||||||
fake._listen();
|
|
||||||
return fake;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listen() {
|
|
||||||
// A connection torn down mid-write during teardown is expected
|
|
||||||
_server.listen((request) => unawaited(_route(request).catchError((_) {})));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _route(HttpRequest request) async {
|
|
||||||
final method = request.method;
|
|
||||||
final path = request.uri.path;
|
|
||||||
|
|
||||||
if (method == 'GET' && path == '/api/server/ping') {
|
|
||||||
return _respondJson(request, {'res': 'pong'});
|
|
||||||
}
|
|
||||||
if (method == 'GET' && path == '/api/server/version') {
|
|
||||||
final (major, minor, patch) = version;
|
|
||||||
return _respondJson(request, {'major': major, 'minor': minor, 'patch': patch});
|
|
||||||
}
|
|
||||||
if (path == '/api/sync/ack') {
|
|
||||||
if (method != 'DELETE') {
|
|
||||||
ackRequests++;
|
|
||||||
}
|
|
||||||
return _respondEmpty(request);
|
|
||||||
}
|
|
||||||
if (method == 'POST' && path == '/api/sync/stream') {
|
|
||||||
return _openSyncStream(request);
|
|
||||||
}
|
|
||||||
return _respondEmpty(request, status: HttpStatus.notFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openSyncStream(HttpRequest request) async {
|
|
||||||
await request.drain<void>();
|
|
||||||
request.response
|
|
||||||
..statusCode = HttpStatus.ok
|
|
||||||
..headers.contentType = ContentType('application', 'jsonlines+json')
|
|
||||||
..contentLength = -1 // chunked: stays open to stream incrementally
|
|
||||||
..bufferOutput = false;
|
|
||||||
// Flush headers so the client's send() resolves and enters its read loop.
|
|
||||||
await request.response.flush();
|
|
||||||
if (!_streamOpened.isCompleted) {
|
|
||||||
_streamOpened.complete(SyncStream._(request.response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _respondJson(HttpRequest request, Object body) async {
|
|
||||||
await request.drain<void>();
|
|
||||||
request.response
|
|
||||||
..statusCode = HttpStatus.ok
|
|
||||||
..headers.contentType = ContentType.json
|
|
||||||
..write(jsonEncode(body));
|
|
||||||
await request.response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _respondEmpty(HttpRequest request, {int status = HttpStatus.ok}) async {
|
|
||||||
await request.drain<void>();
|
|
||||||
request.response.statusCode = status;
|
|
||||||
await request.response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_streamOpened.isCompleted) {
|
|
||||||
await (await _streamOpened.future).close();
|
|
||||||
}
|
|
||||||
await _server.close(force: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle to the open `/sync/stream` response: push jsonlines events, then end.
|
|
||||||
class SyncStream {
|
|
||||||
SyncStream._(this._response);
|
|
||||||
|
|
||||||
final HttpResponse _response;
|
|
||||||
bool _closed = false;
|
|
||||||
|
|
||||||
/// [data] should be a Sync*V1 DTO's `toJson()` so the parser's `fromJson` round-trips it.
|
|
||||||
void send({required String type, required Object data, required String ack}) {
|
|
||||||
if (_closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_response.write('${jsonEncode({'type': type, 'data': data, 'ack': ack})}\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_closed = true;
|
|
||||||
await _response.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -121,8 +121,8 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the currently running background task, either due to timeout or external request.
|
* Cancels the currently running background task, either due to timeout or external request.
|
||||||
* Only tears down the engine after Dart confirms it's drained. If Dart overruns iOS's grace window,
|
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
||||||
* the expiration handler still calls setTaskCompleted and iOS suspends us.
|
* the completion handler is eventually called even if Flutter doesn't respond.
|
||||||
*/
|
*/
|
||||||
func close() {
|
func close() {
|
||||||
if isComplete {
|
if isComplete {
|
||||||
@@ -132,6 +132,12 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
flutterApi?.cancel { result in
|
flutterApi?.cancel { result in
|
||||||
self.complete(success: false)
|
self.complete(success: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback safety mechanism: ensure completion is called within 2 seconds
|
||||||
|
// This prevents the background task from hanging indefinitely if Flutter doesn't respond
|
||||||
|
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||||
|
self.complete(success: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-81
@@ -11,24 +11,6 @@ import Foundation
|
|||||||
#error("Unsupported platform.")
|
#error("Unsupported platform.")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Error class for passing custom error details to Dart side.
|
|
||||||
final class PigeonError: Error {
|
|
||||||
let code: String
|
|
||||||
let message: String?
|
|
||||||
let details: Sendable?
|
|
||||||
|
|
||||||
init(code: String, message: String?, details: Sendable?) {
|
|
||||||
self.code = code
|
|
||||||
self.message = message
|
|
||||||
self.details = details
|
|
||||||
}
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
return
|
|
||||||
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func wrapResult(_ result: Any?) -> [Any?] {
|
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||||
return [result]
|
return [result]
|
||||||
}
|
}
|
||||||
@@ -64,57 +46,8 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
|
|||||||
return value as! T?
|
return value as! T?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum PermissionStatus: Int {
|
|
||||||
case granted = 0
|
|
||||||
case denied = 1
|
|
||||||
case permanentlyDenied = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PermissionApiPigeonCodecReader: FlutterStandardReader {
|
|
||||||
override func readValue(ofType type: UInt8) -> Any? {
|
|
||||||
switch type {
|
|
||||||
case 129:
|
|
||||||
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
|
|
||||||
if let enumResultAsInt = enumResultAsInt {
|
|
||||||
return PermissionStatus(rawValue: enumResultAsInt)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return super.readValue(ofType: type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PermissionApiPigeonCodecWriter: FlutterStandardWriter {
|
|
||||||
override func writeValue(_ value: Any) {
|
|
||||||
if let value = value as? PermissionStatus {
|
|
||||||
super.writeByte(129)
|
|
||||||
super.writeValue(value.rawValue)
|
|
||||||
} else {
|
|
||||||
super.writeValue(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PermissionApiPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
|
||||||
override func reader(with data: Data) -> FlutterStandardReader {
|
|
||||||
return PermissionApiPigeonCodecReader(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
|
||||||
return PermissionApiPigeonCodecWriter(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
|
||||||
static let shared = PermissionApiPigeonCodec(readerWriter: PermissionApiPigeonCodecReaderWriter())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol PermissionApi {
|
protocol PermissionApi {
|
||||||
func isIgnoringBatteryOptimizations() throws -> PermissionStatus
|
|
||||||
func hasManageMediaPermission() throws -> Bool
|
func hasManageMediaPermission() throws -> Bool
|
||||||
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
@@ -122,23 +55,10 @@ protocol PermissionApi {
|
|||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
class PermissionApiSetup {
|
class PermissionApiSetup {
|
||||||
static var codec: FlutterStandardMessageCodec { PermissionApiPigeonCodec.shared }
|
static var codec: FlutterStandardMessageCodec { FlutterStandardMessageCodec.sharedInstance() }
|
||||||
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
|
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
let isIgnoringBatteryOptimizationsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
isIgnoringBatteryOptimizationsChannel.setMessageHandler { _, reply in
|
|
||||||
do {
|
|
||||||
let result = try api.isIgnoringBatteryOptimizations()
|
|
||||||
reply(wrapResult(result))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isIgnoringBatteryOptimizationsChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
|
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PermissionApiImpl: PermissionApi {
|
class PermissionApiImpl: PermissionApi {
|
||||||
func isIgnoringBatteryOptimizations() throws -> PermissionStatus {
|
|
||||||
return PermissionStatus.granted;
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasManageMediaPermission() throws -> Bool {
|
func hasManageMediaPermission() throws -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+42
-58
@@ -526,17 +526,16 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
|||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol NativeSyncApi {
|
protocol NativeSyncApi {
|
||||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void)
|
func shouldFullSync() throws -> Bool
|
||||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void)
|
func getMediaChanges() throws -> SyncDelta
|
||||||
func checkpointSync() throws
|
func checkpointSync() throws
|
||||||
func clearSyncCheckpoint() throws
|
func clearSyncCheckpoint() throws
|
||||||
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void)
|
func getAssetIdsForAlbum(albumId: String) throws -> [String]
|
||||||
func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void)
|
func getAlbums() throws -> [PlatformAlbum]
|
||||||
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void)
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
|
||||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||||
func cancelHashing() throws
|
func cancelHashing() throws
|
||||||
func cancelSync() throws
|
|
||||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||||
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||||
@@ -556,28 +555,26 @@ class NativeSyncApiSetup {
|
|||||||
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
shouldFullSyncChannel.setMessageHandler { _, reply in
|
shouldFullSyncChannel.setMessageHandler { _, reply in
|
||||||
api.shouldFullSync { result in
|
do {
|
||||||
switch result {
|
let result = try api.shouldFullSync()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shouldFullSyncChannel.setMessageHandler(nil)
|
shouldFullSyncChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getMediaChangesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getMediaChangesChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getMediaChangesChannel.setMessageHandler { _, reply in
|
getMediaChangesChannel.setMessageHandler { _, reply in
|
||||||
api.getMediaChanges { result in
|
do {
|
||||||
switch result {
|
let result = try api.getMediaChanges()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -609,33 +606,33 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
clearSyncCheckpointChannel.setMessageHandler(nil)
|
clearSyncCheckpointChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAssetIdsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getAssetIdsForAlbumChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
let args = message as! [Any?]
|
let args = message as! [Any?]
|
||||||
let albumIdArg = args[0] as! String
|
let albumIdArg = args[0] as! String
|
||||||
api.getAssetIdsForAlbum(albumId: albumIdArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getAssetIdsForAlbumChannel.setMessageHandler(nil)
|
getAssetIdsForAlbumChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAlbumsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getAlbumsChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAlbumsChannel.setMessageHandler { _, reply in
|
getAlbumsChannel.setMessageHandler { _, reply in
|
||||||
api.getAlbums { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAlbums()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -659,19 +656,19 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
getAssetsCountSinceChannel.setMessageHandler(nil)
|
getAssetsCountSinceChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAssetsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let getAssetsForAlbumChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAssetsForAlbumChannel.setMessageHandler { message, reply in
|
getAssetsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
let args = message as! [Any?]
|
let args = message as! [Any?]
|
||||||
let albumIdArg = args[0] as! String
|
let albumIdArg = args[0] as! String
|
||||||
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
|
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
|
||||||
api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -710,19 +707,6 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
cancelHashingChannel.setMessageHandler(nil)
|
cancelHashingChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let cancelSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
|
||||||
if let api = api {
|
|
||||||
cancelSyncChannel.setMessageHandler { _, reply in
|
|
||||||
do {
|
|
||||||
try api.cancelSync()
|
|
||||||
reply(wrapResult(nil))
|
|
||||||
} catch {
|
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cancelSyncChannel.setMessageHandler(nil)
|
|
||||||
}
|
|
||||||
let getTrashedAssetsChannel = taskQueue == nil
|
let getTrashedAssetsChannel = taskQueue == nil
|
||||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
private static let hashCancelledCode = "HASH_CANCELLED"
|
private static let hashCancelledCode = "HASH_CANCELLED"
|
||||||
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
||||||
|
|
||||||
private var syncTask: Task<Void?, Error>?
|
|
||||||
private static let syncCancelledCode = "SYNC_CANCELLED"
|
|
||||||
private static let syncCancelled = PigeonError(code: syncCancelledCode, message: "Sync cancelled", details: nil)
|
|
||||||
|
|
||||||
init(with defaults: UserDefaults = .standard) {
|
init(with defaults: UserDefaults = .standard) {
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
@@ -74,11 +71,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
|
func shouldFullSync() -> Bool {
|
||||||
runSync(completion) { $0.shouldFullSync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldFullSync() -> Bool {
|
|
||||||
guard #available(iOS 16, *),
|
guard #available(iOS 16, *),
|
||||||
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
||||||
let storedToken = getChangeToken() else {
|
let storedToken = getChangeToken() else {
|
||||||
@@ -94,17 +87,12 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void) {
|
func getAlbums() throws -> [PlatformAlbum] {
|
||||||
runSync(completion) { try $0.getAlbums() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAlbums() throws -> [PlatformAlbum] {
|
|
||||||
var albums: [PlatformAlbum] = []
|
var albums: [PlatformAlbum] = []
|
||||||
|
|
||||||
for type in albumTypes {
|
albumTypes.forEach { type in
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||||
for i in 0..<collections.count {
|
for i in 0..<collections.count {
|
||||||
try Task.checkCancellation()
|
|
||||||
let album = collections.object(at: i)
|
let album = collections.object(at: i)
|
||||||
|
|
||||||
// Ignore recovered album
|
// Ignore recovered album
|
||||||
@@ -138,11 +126,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return albums.sorted { $0.id < $1.id }
|
return albums.sorted { $0.id < $1.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
|
func getMediaChanges() throws -> SyncDelta {
|
||||||
runSync(completion) { try $0.getMediaChanges() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getMediaChanges() throws -> SyncDelta {
|
|
||||||
guard #available(iOS 16, *) else {
|
guard #available(iOS 16, *) else {
|
||||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
||||||
}
|
}
|
||||||
@@ -162,49 +146,51 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
do {
|
||||||
|
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||||
|
|
||||||
var updatedAssets: Set<AssetWrapper> = []
|
var updatedAssets: Set<AssetWrapper> = []
|
||||||
var deletedAssets: Set<String> = []
|
var deletedAssets: Set<String> = []
|
||||||
|
|
||||||
for change in changes {
|
for change in changes {
|
||||||
try Task.checkCancellation()
|
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
||||||
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
|
||||||
|
|
||||||
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
||||||
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
||||||
|
|
||||||
if (updated.isEmpty) { continue }
|
if (updated.isEmpty) { continue }
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
||||||
for i in 0..<result.count {
|
for i in 0..<result.count {
|
||||||
let asset = result.object(at: i)
|
let asset = result.object(at: i)
|
||||||
|
|
||||||
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
||||||
let predicate = PlatformAsset(
|
let predicate = PlatformAsset(
|
||||||
id: asset.localIdentifier,
|
id: asset.localIdentifier,
|
||||||
name: "",
|
name: "",
|
||||||
type: 0,
|
type: 0,
|
||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
orientation: 0,
|
orientation: 0,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
playbackStyle: .unknown
|
playbackStyle: .unknown
|
||||||
)
|
)
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
||||||
|
updatedAssets.insert(domainAsset)
|
||||||
}
|
}
|
||||||
|
|
||||||
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
|
||||||
updatedAssets.insert(domainAsset)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let updates = Array(updatedAssets.map { $0.asset })
|
let updates = Array(updatedAssets.map { $0.asset })
|
||||||
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
||||||
guard !assets.isEmpty else {
|
guard !assets.isEmpty else {
|
||||||
return [:]
|
return [:]
|
||||||
@@ -227,11 +213,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return albumAssets
|
return albumAssets
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) {
|
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
||||||
runSync(completion) { try $0.getAssetIdsForAlbum(albumId: albumId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return []
|
return []
|
||||||
@@ -241,14 +223,9 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||||
assets.enumerateObjects { (asset, _, stop) in
|
assets.enumerateObjects { (asset, _, _) in
|
||||||
if Task.isCancelled {
|
|
||||||
stop.pointee = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ids.append(asset.localIdentifier)
|
ids.append(asset.localIdentifier)
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,11 +243,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
return Int64(assets.count)
|
return Int64(assets.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void) {
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
||||||
runSync(completion) { try $0.getAssetsForAlbum(albumId: albumId, updatedTimeCond: updatedTimeCond) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return []
|
return []
|
||||||
@@ -289,14 +262,9 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var assets: [PlatformAsset] = []
|
var assets: [PlatformAsset] = []
|
||||||
result.enumerateObjects { (asset, _, stop) in
|
result.enumerateObjects { (asset, _, _) in
|
||||||
if Task.isCancelled {
|
|
||||||
stop.pointee = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assets.append(asset.toPlatformAsset())
|
assets.append(asset.toPlatformAsset())
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
|
||||||
return assets
|
return assets
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,31 +324,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
hashTask = nil
|
hashTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelSync() {
|
|
||||||
syncTask?.cancel()
|
|
||||||
syncTask = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func runSync<T>(
|
|
||||||
_ completion: @escaping (Result<T, Error>) -> Void,
|
|
||||||
_ work: @escaping (NativeSyncApiImpl) throws -> T
|
|
||||||
) {
|
|
||||||
syncTask?.cancel()
|
|
||||||
syncTask = Task { [weak self] in
|
|
||||||
guard let self else { return nil }
|
|
||||||
let result: Result<T, Error>
|
|
||||||
do {
|
|
||||||
result = .success(try work(self))
|
|
||||||
} catch is CancellationError {
|
|
||||||
result = .failure(Self.syncCancelled)
|
|
||||||
} catch {
|
|
||||||
result = .failure(error)
|
|
||||||
}
|
|
||||||
self.completeWhenActive(for: completion, with: result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
||||||
class RequestRef {
|
class RequestRef {
|
||||||
var id: PHAssetResourceDataRequestID?
|
var id: PHAssetResourceDataRequestID?
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:immich_mobile/utils/option.dart';
|
|
||||||
|
|
||||||
const defaultConfig = AppConfig();
|
const defaultConfig = AppConfig();
|
||||||
|
|
||||||
@@ -131,8 +130,6 @@ class AppConfig {
|
|||||||
.mapIncludeArchived => map.includeArchived,
|
.mapIncludeArchived => map.includeArchived,
|
||||||
.mapThemeMode => map.themeMode,
|
.mapThemeMode => map.themeMode,
|
||||||
.mapWithPartners => map.withPartners,
|
.mapWithPartners => map.withPartners,
|
||||||
.mapCustomFrom => map.customFrom,
|
|
||||||
.mapCustomTo => map.customTo,
|
|
||||||
.cleanupKeepFavorites => cleanup.keepFavorites,
|
.cleanupKeepFavorites => cleanup.keepFavorites,
|
||||||
.cleanupKeepMediaType => cleanup.keepMediaType,
|
.cleanupKeepMediaType => cleanup.keepMediaType,
|
||||||
.cleanupKeepAlbumIds => cleanup.keepAlbumIds,
|
.cleanupKeepAlbumIds => cleanup.keepAlbumIds,
|
||||||
@@ -184,8 +181,6 @@ class AppConfig {
|
|||||||
.mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)),
|
.mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)),
|
||||||
.mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)),
|
.mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)),
|
||||||
.mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)),
|
.mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)),
|
||||||
.mapCustomFrom => copyWith(map: map.copyWith(customFrom: value as Option<DateTime>)),
|
|
||||||
.mapCustomTo => copyWith(map: map.copyWith(customTo: value as Option<DateTime>)),
|
|
||||||
.cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)),
|
.cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)),
|
||||||
.cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)),
|
.cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)),
|
||||||
.cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List<String>)),
|
.cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List<String>)),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/utils/option.dart';
|
|
||||||
|
|
||||||
class MapConfig {
|
class MapConfig {
|
||||||
final int relativeDays;
|
final int relativeDays;
|
||||||
@@ -7,8 +6,6 @@ class MapConfig {
|
|||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
final Option<DateTime> customFrom;
|
|
||||||
final Option<DateTime> customTo;
|
|
||||||
|
|
||||||
const MapConfig({
|
const MapConfig({
|
||||||
this.relativeDays = 0,
|
this.relativeDays = 0,
|
||||||
@@ -16,8 +13,6 @@ class MapConfig {
|
|||||||
this.includeArchived = false,
|
this.includeArchived = false,
|
||||||
this.themeMode = ThemeMode.system,
|
this.themeMode = ThemeMode.system,
|
||||||
this.withPartners = false,
|
this.withPartners = false,
|
||||||
this.customFrom = const Option.none(),
|
|
||||||
this.customTo = const Option.none(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MapConfig copyWith({
|
MapConfig copyWith({
|
||||||
@@ -26,16 +21,12 @@ class MapConfig {
|
|||||||
bool? includeArchived,
|
bool? includeArchived,
|
||||||
ThemeMode? themeMode,
|
ThemeMode? themeMode,
|
||||||
bool? withPartners,
|
bool? withPartners,
|
||||||
Option<DateTime>? customFrom,
|
|
||||||
Option<DateTime>? customTo,
|
|
||||||
}) => MapConfig(
|
}) => MapConfig(
|
||||||
relativeDays: relativeDays ?? this.relativeDays,
|
relativeDays: relativeDays ?? this.relativeDays,
|
||||||
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
|
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
|
||||||
includeArchived: includeArchived ?? this.includeArchived,
|
includeArchived: includeArchived ?? this.includeArchived,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
withPartners: withPartners ?? this.withPartners,
|
withPartners: withPartners ?? this.withPartners,
|
||||||
customFrom: customFrom ?? this.customFrom,
|
|
||||||
customTo: customTo ?? this.customTo,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -46,15 +37,12 @@ class MapConfig {
|
|||||||
other.favoritesOnly == favoritesOnly &&
|
other.favoritesOnly == favoritesOnly &&
|
||||||
other.includeArchived == includeArchived &&
|
other.includeArchived == includeArchived &&
|
||||||
other.themeMode == themeMode &&
|
other.themeMode == themeMode &&
|
||||||
other.withPartners == withPartners &&
|
other.withPartners == withPartners);
|
||||||
other.customFrom == customFrom &&
|
|
||||||
other.customTo == customTo);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
|
||||||
Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners, customFrom, customTo);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners, customFrom: $customFrom, customTo: $customTo)';
|
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:immich_mobile/utils/option.dart';
|
|
||||||
|
|
||||||
enum SettingsKey<T extends Object> {
|
enum SettingsKey<T extends Object> {
|
||||||
// Theme
|
// Theme
|
||||||
@@ -59,8 +58,6 @@ enum SettingsKey<T extends Object> {
|
|||||||
mapIncludeArchived<bool>(),
|
mapIncludeArchived<bool>(),
|
||||||
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||||
mapWithPartners<bool>(),
|
mapWithPartners<bool>(),
|
||||||
mapCustomFrom<Option<DateTime>>(codec: _OptionCodec(_DateTimeCodec())),
|
|
||||||
mapCustomTo<Option<DateTime>>(codec: _OptionCodec(_DateTimeCodec())),
|
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanupKeepFavorites<bool>(),
|
cleanupKeepFavorites<bool>(),
|
||||||
@@ -132,30 +129,6 @@ final class _DateTimeCodec extends _SettingsCodec<DateTime> {
|
|||||||
DateTime decode(String raw) => DateTime.parse(raw);
|
DateTime decode(String raw) => DateTime.parse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _OptionCodec<T extends Object> extends _SettingsCodec<Option<T>> {
|
|
||||||
final _SettingsCodec<T> _inner;
|
|
||||||
|
|
||||||
const _OptionCodec(this._inner);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String encode(Option<T> value) => switch (value) {
|
|
||||||
Some(:final value) => _inner.encode(value),
|
|
||||||
None() => '',
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Option<T> decode(String raw) {
|
|
||||||
if (raw.isEmpty) {
|
|
||||||
return const Option.none();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Option.some(_inner.decode(raw));
|
|
||||||
} on FormatException {
|
|
||||||
return const Option.none();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
|
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
|
||||||
final _SettingsCodec<K> _keyCodec;
|
final _SettingsCodec<K> _keyCodec;
|
||||||
final _SettingsCodec<V> _valueCodec;
|
final _SettingsCodec<V> _valueCodec;
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import 'package:immich_mobile/utils/option.dart';
|
|
||||||
|
|
||||||
class TimeRange {
|
|
||||||
final Option<DateTime> from;
|
|
||||||
final Option<DateTime> to;
|
|
||||||
|
|
||||||
const TimeRange({this.from = const None(), this.to = const None()});
|
|
||||||
|
|
||||||
TimeRange copyWith({Option<DateTime>? from, Option<DateTime>? to}) {
|
|
||||||
return TimeRange(from: from ?? this.from, to: to ?? this.to);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeRange clearFrom() => TimeRange(to: to);
|
|
||||||
TimeRange clearTo() => TimeRange(from: from);
|
|
||||||
}
|
|
||||||
@@ -113,35 +113,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||||
final sw = Stopwatch()..start();
|
final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
|
||||||
try {
|
return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
|
||||||
final budget = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
|
|
||||||
|
|
||||||
final sync = _ref?.read(backgroundSyncProvider);
|
|
||||||
if (sync == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run sync local, sync remote, hash and backup concurrently so the bg
|
|
||||||
// refresh task (20s budget) can make progress on all four instead of
|
|
||||||
// racing them sequentially. Phases are independent at the data layer:
|
|
||||||
// hash and handle_backup read drift state and tolerate stale reads
|
|
||||||
// (server-side dedup catches the rare race). The single budget caps the
|
|
||||||
// whole batch; no phase needs its own timeout.
|
|
||||||
final all = Future.wait<dynamic>([sync.syncLocal(), sync.syncRemote(), sync.hashAssets(), _handleBackup()]);
|
|
||||||
if (budget != null) {
|
|
||||||
await all.timeout(budget, onTimeout: () => <dynamic>[]);
|
|
||||||
} else {
|
|
||||||
await all;
|
|
||||||
}
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe("Failed to complete iOS background upload", error, stack);
|
|
||||||
} finally {
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
await _cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _backgroundLoop({
|
Future<void> _backgroundLoop({
|
||||||
@@ -214,14 +188,20 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
if (!_cancellationToken.isCompleted) {
|
if (!_cancellationToken.isCompleted) {
|
||||||
_cancellationToken.complete();
|
_cancellationToken.complete();
|
||||||
}
|
}
|
||||||
|
final cleanupFutures = [
|
||||||
|
nativeSyncApi?.cancelHashing(),
|
||||||
|
workerManagerPatch.dispose().catchError((_) async {
|
||||||
|
// Discard any errors on the dispose call
|
||||||
|
return;
|
||||||
|
}),
|
||||||
|
LogService.I.dispose(),
|
||||||
|
Store.dispose(),
|
||||||
|
|
||||||
// Workers share one sqlite connection, so DB teardown must wait until every worker has stopped using it.
|
backgroundSyncManager?.cancel(),
|
||||||
await Future.wait([
|
_drift.optimize(allTables: true),
|
||||||
if (backgroundSyncManager != null) backgroundSyncManager.cancel(),
|
];
|
||||||
if (nativeSyncApi != null) nativeSyncApi.cancelHashing(),
|
|
||||||
]);
|
await Future.wait(cleanupFutures.nonNulls);
|
||||||
await workerManagerPatch.dispose().catchError((_) async {});
|
|
||||||
await Future.wait([LogService.I.dispose(), Store.dispose(), _drift.optimize(allTables: true)]);
|
|
||||||
await _drift.close();
|
await _drift.close();
|
||||||
await _driftLogger.close();
|
await _driftLogger.close();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
@@ -19,7 +17,7 @@ class HashService {
|
|||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
final Completer<void>? _cancellation;
|
final bool Function()? _cancelChecker;
|
||||||
final _log = Logger('HashService');
|
final _log = Logger('HashService');
|
||||||
|
|
||||||
HashService({
|
HashService({
|
||||||
@@ -27,15 +25,11 @@ class HashService {
|
|||||||
required this._localAssetRepository,
|
required this._localAssetRepository,
|
||||||
required this._trashedLocalAssetRepository,
|
required this._trashedLocalAssetRepository,
|
||||||
required this._nativeSyncApi,
|
required this._nativeSyncApi,
|
||||||
this._cancellation,
|
this._cancelChecker,
|
||||||
int? batchSize,
|
int? batchSize,
|
||||||
}) : _batchSize = batchSize ?? kBatchHashFileLimit {
|
}) : _batchSize = batchSize ?? kBatchHashFileLimit;
|
||||||
// Stop the in-flight native hash call promptly on cancellation; the loops
|
|
||||||
// below also observe [isCancelled] to bail between batches.
|
|
||||||
_cancellation?.future.then((_) => _nativeSyncApi.cancelHashing().onError(_log.warning));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isCancelled => _cancellation?.isCompleted ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> hashAssets() async {
|
Future<void> hashAssets() async {
|
||||||
_log.info("Starting hashing of assets");
|
_log.info("Starting hashing of assets");
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_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/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
@@ -18,8 +17,6 @@ import 'package:immich_mobile/utils/datetime_helpers.dart';
|
|||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
const String _kSyncCancelledCode = "SYNC_CANCELLED";
|
|
||||||
|
|
||||||
class LocalSyncService {
|
class LocalSyncService {
|
||||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
@@ -28,7 +25,6 @@ class LocalSyncService {
|
|||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final AssetMediaRepository _assetMediaRepository;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
final IPermissionRepository _permissionRepository;
|
final IPermissionRepository _permissionRepository;
|
||||||
final Completer<void>? _cancellation;
|
|
||||||
final Logger _log = Logger("DeviceSyncService");
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
LocalSyncService({
|
LocalSyncService({
|
||||||
@@ -38,12 +34,7 @@ class LocalSyncService {
|
|||||||
required this._trashedLocalAssetRepository,
|
required this._trashedLocalAssetRepository,
|
||||||
required this._assetMediaRepository,
|
required this._assetMediaRepository,
|
||||||
required this._permissionRepository,
|
required this._permissionRepository,
|
||||||
this._cancellation,
|
});
|
||||||
}) {
|
|
||||||
_cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isCancelled => _cancellation?.isCompleted ?? false;
|
|
||||||
|
|
||||||
Future<void> sync({bool full = false}) async {
|
Future<void> sync({bool full = false}) async {
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
@@ -90,10 +81,6 @@ class LocalSyncService {
|
|||||||
// detect album deletions from the native side
|
// detect album deletions from the native side
|
||||||
if (CurrentPlatform.isAndroid) {
|
if (CurrentPlatform.isAndroid) {
|
||||||
for (final album in dbAlbums) {
|
for (final album in dbAlbums) {
|
||||||
if (_isCancelled) {
|
|
||||||
_log.warning("Local sync cancelled. Stopped processing albums.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
|
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
|
||||||
await _localAlbumRepository.syncDeletes(album.id, deviceIds);
|
await _localAlbumRepository.syncDeletes(album.id, deviceIds);
|
||||||
}
|
}
|
||||||
@@ -104,10 +91,6 @@ class LocalSyncService {
|
|||||||
// does not include changes for cloud albums.
|
// does not include changes for cloud albums.
|
||||||
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
|
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
|
||||||
for (final album in cloudAlbums) {
|
for (final album in cloudAlbums) {
|
||||||
if (_isCancelled) {
|
|
||||||
_log.warning("Local sync cancelled. Stopped processing cloud albums.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
|
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
|
||||||
if (dbAlbum == null) {
|
if (dbAlbum == null) {
|
||||||
_log.warning("Cloud album ${album.name} not found in local database. Skipping sync.");
|
_log.warning("Cloud album ${album.name} not found in local database. Skipping sync.");
|
||||||
@@ -119,12 +102,6 @@ class LocalSyncService {
|
|||||||
await _mapIosCloudIds(newAssets);
|
await _mapIosCloudIds(newAssets);
|
||||||
}
|
}
|
||||||
await _nativeSyncApi.checkpointSync();
|
await _nativeSyncApi.checkpointSync();
|
||||||
} on PlatformException catch (e, s) {
|
|
||||||
if (e.code == _kSyncCancelledCode) {
|
|
||||||
_log.warning("Local sync cancelled");
|
|
||||||
} else {
|
|
||||||
_log.severe("Error performing device sync", e, s);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_log.severe("Error performing device sync", e, s);
|
_log.severe("Error performing device sync", e, s);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -152,21 +129,12 @@ class LocalSyncService {
|
|||||||
await _nativeSyncApi.checkpointSync();
|
await _nativeSyncApi.checkpointSync();
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
} on PlatformException catch (e, s) {
|
|
||||||
if (e.code == _kSyncCancelledCode) {
|
|
||||||
_log.warning("Full device sync cancelled");
|
|
||||||
} else {
|
|
||||||
_log.severe("Error performing full device sync", e, s);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_log.severe("Error performing full device sync", e, s);
|
_log.severe("Error performing full device sync", e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAlbum(LocalAlbum album) async {
|
Future<void> addAlbum(LocalAlbum album) async {
|
||||||
if (_isCancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
_log.fine("Adding device album ${album.name}");
|
_log.fine("Adding device album ${album.name}");
|
||||||
|
|
||||||
@@ -194,9 +162,6 @@ class LocalSyncService {
|
|||||||
|
|
||||||
// The deviceAlbum is ignored since we are going to refresh it anyways
|
// The deviceAlbum is ignored since we are going to refresh it anyways
|
||||||
FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||||
if (_isCancelled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
_log.fine("Syncing device album ${dbAlbum.name}");
|
_log.fine("Syncing device album ${dbAlbum.name}");
|
||||||
|
|
||||||
|
|||||||
@@ -112,16 +112,10 @@ class LogService {
|
|||||||
return _flushBuffer();
|
return _flushBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() {
|
||||||
_flushTimer?.cancel();
|
_flushTimer?.cancel();
|
||||||
_flushTimer = null;
|
_logSubscription.cancel();
|
||||||
await _logSubscription.cancel();
|
return _flushBuffer();
|
||||||
await _flushBuffer();
|
|
||||||
// Allow a subsequent init() (e.g. when a worker isolate is reused) to
|
|
||||||
// create a fresh instance instead of returning this disposed one.
|
|
||||||
if (identical(_instance, this)) {
|
|
||||||
_instance = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _flushBuffer() async {
|
Future<void> _flushBuffer() async {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:openapi/api.dart' show Optional;
|
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -138,7 +137,7 @@ class RemoteAlbumService {
|
|||||||
Future<RemoteAlbum> updateAlbum(
|
Future<RemoteAlbum> updateAlbum(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
String? name,
|
String? name,
|
||||||
Optional<String?> description = const Optional.absent(),
|
String? description,
|
||||||
String? thumbnailAssetId,
|
String? thumbnailAssetId,
|
||||||
bool? isActivityEnabled,
|
bool? isActivityEnabled,
|
||||||
AlbumAssetOrder? order,
|
AlbumAssetOrder? order,
|
||||||
|
|||||||
@@ -54,13 +54,7 @@ class StoreService {
|
|||||||
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _storeUpdateSubscription?.cancel();
|
await _storeUpdateSubscription?.cancel();
|
||||||
_storeUpdateSubscription = null;
|
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
// Allow a subsequent init() (e.g. when a worker isolate is reused) to
|
|
||||||
// create a fresh instance instead of returning this disposed one.
|
|
||||||
if (identical(_instance, this)) {
|
|
||||||
_instance = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the cached value for [key], or `null`
|
/// Returns the cached value for [key], or `null`
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
@@ -7,7 +5,6 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
@@ -19,7 +16,6 @@ final syncLinkedAlbumServiceProvider = Provider(
|
|||||||
ref.watch(remoteAlbumRepository),
|
ref.watch(remoteAlbumRepository),
|
||||||
ref.watch(driftAlbumApiRepositoryProvider),
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
ref.watch(storeServiceProvider),
|
ref.watch(storeServiceProvider),
|
||||||
cancellation: ref.watch(cancellationProvider),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -28,15 +24,13 @@ class SyncLinkedAlbumService {
|
|||||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
final DriftAlbumApiRepository _albumApiRepository;
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
final StoreService _storeService;
|
final StoreService _storeService;
|
||||||
final Completer<void>? _cancellation;
|
|
||||||
|
|
||||||
SyncLinkedAlbumService(
|
SyncLinkedAlbumService(
|
||||||
this._localAlbumRepository,
|
this._localAlbumRepository,
|
||||||
this._remoteAlbumRepository,
|
this._remoteAlbumRepository,
|
||||||
this._albumApiRepository,
|
this._albumApiRepository,
|
||||||
this._storeService, {
|
this._storeService,
|
||||||
this._cancellation,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
final _log = Logger("SyncLinkedAlbumService");
|
final _log = Logger("SyncLinkedAlbumService");
|
||||||
|
|
||||||
@@ -61,11 +55,7 @@ class SyncLinkedAlbumService {
|
|||||||
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
||||||
_log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}");
|
_log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}");
|
||||||
if (assetIds.isNotEmpty) {
|
if (assetIds.isNotEmpty) {
|
||||||
final album = await _albumApiRepository.addAssets(
|
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
|
||||||
remoteAlbum.id,
|
|
||||||
assetIds,
|
|
||||||
abortTrigger: _cancellation?.future,
|
|
||||||
);
|
|
||||||
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class SyncStreamService {
|
|||||||
final IPermissionRepository _permissionRepository;
|
final IPermissionRepository _permissionRepository;
|
||||||
final SyncMigrationRepository _syncMigrationRepository;
|
final SyncMigrationRepository _syncMigrationRepository;
|
||||||
final ApiService _api;
|
final ApiService _api;
|
||||||
final Completer<void>? _cancellation;
|
final bool Function()? _cancelChecker;
|
||||||
|
|
||||||
SyncStreamService({
|
SyncStreamService({
|
||||||
required this._syncApiRepository,
|
required this._syncApiRepository,
|
||||||
@@ -49,10 +49,10 @@ class SyncStreamService {
|
|||||||
required this._permissionRepository,
|
required this._permissionRepository,
|
||||||
required this._syncMigrationRepository,
|
required this._syncMigrationRepository,
|
||||||
required this._api,
|
required this._api,
|
||||||
this._cancellation,
|
this._cancelChecker,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isCancelled => _cancellation?.isCompleted ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<bool> sync() async {
|
Future<bool> sync() async {
|
||||||
_logger.info("Remote sync request for user");
|
_logger.info("Remote sync request for user");
|
||||||
@@ -80,15 +80,10 @@ class SyncStreamService {
|
|||||||
_handleEvents,
|
_handleEvents,
|
||||||
serverVersion: serverSemVer,
|
serverVersion: serverSemVer,
|
||||||
onReset: () => shouldReset = true,
|
onReset: () => shouldReset = true,
|
||||||
abortSignal: _cancellation?.future,
|
|
||||||
);
|
);
|
||||||
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(
|
await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer);
|
||||||
_handleEvents,
|
|
||||||
serverVersion: serverSemVer,
|
|
||||||
abortSignal: _cancellation?.future,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
previousLength = migrations.length;
|
previousLength = migrations.length;
|
||||||
@@ -323,7 +318,7 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
|
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
|
||||||
if (batchData.isEmpty || isCancelled) {
|
if (batchData.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +361,7 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
|
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
|
||||||
if (batchData.isEmpty || isCancelled) {
|
if (batchData.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,9 +404,6 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleWsAssetEditReadyV1(dynamic data) async {
|
Future<void> handleWsAssetEditReadyV1(dynamic data) async {
|
||||||
if (isCancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_logger.info('Processing AssetEditReadyV1 event');
|
_logger.info('Processing AssetEditReadyV1 event');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -452,9 +444,6 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleWsAssetEditReadyV2(dynamic data) async {
|
Future<void> handleWsAssetEditReadyV2(dynamic data) async {
|
||||||
if (isCancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_logger.info('Processing AssetEditReadyV2 event');
|
_logger.info('Processing AssetEditReadyV2 event');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -50,28 +50,54 @@ class BackgroundSyncManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Future<void> cancel() async {
|
Future<void> cancel() async {
|
||||||
final tasks = [
|
final futures = <Future>[];
|
||||||
_syncTask,
|
|
||||||
_syncWebsocketTask,
|
if (_syncTask != null) {
|
||||||
_cloudIdSyncTask,
|
futures.add(_syncTask!.future);
|
||||||
_linkedAlbumSyncTask,
|
|
||||||
_deviceAlbumSyncTask,
|
|
||||||
_hashTask,
|
|
||||||
];
|
|
||||||
final futures = [
|
|
||||||
for (final task in tasks)
|
|
||||||
if (task != null) task.future,
|
|
||||||
];
|
|
||||||
for (final task in tasks) {
|
|
||||||
task?.cancel();
|
|
||||||
}
|
}
|
||||||
|
_syncTask?.cancel();
|
||||||
_syncTask = null;
|
_syncTask = null;
|
||||||
|
|
||||||
|
if (_syncWebsocketTask != null) {
|
||||||
|
futures.add(_syncWebsocketTask!.future);
|
||||||
|
}
|
||||||
|
_syncWebsocketTask?.cancel();
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
|
|
||||||
|
if (_cloudIdSyncTask != null) {
|
||||||
|
futures.add(_cloudIdSyncTask!.future);
|
||||||
|
}
|
||||||
|
_cloudIdSyncTask?.cancel();
|
||||||
_cloudIdSyncTask = null;
|
_cloudIdSyncTask = null;
|
||||||
|
|
||||||
|
if (_linkedAlbumSyncTask != null) {
|
||||||
|
futures.add(_linkedAlbumSyncTask!.future);
|
||||||
|
}
|
||||||
|
_linkedAlbumSyncTask?.cancel();
|
||||||
_linkedAlbumSyncTask = null;
|
_linkedAlbumSyncTask = null;
|
||||||
_deviceAlbumSyncTask = null;
|
|
||||||
|
try {
|
||||||
|
await Future.wait(futures);
|
||||||
|
} on CanceledError {
|
||||||
|
// Ignore cancellation errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelLocal() async {
|
||||||
|
final futures = <Future>[];
|
||||||
|
|
||||||
|
if (_hashTask != null) {
|
||||||
|
futures.add(_hashTask!.future);
|
||||||
|
}
|
||||||
|
_hashTask?.cancel();
|
||||||
_hashTask = null;
|
_hashTask = null;
|
||||||
|
|
||||||
|
if (_deviceAlbumSyncTask != null) {
|
||||||
|
futures.add(_deviceAlbumSyncTask!.future);
|
||||||
|
}
|
||||||
|
_deviceAlbumSyncTask?.cancel();
|
||||||
|
_deviceAlbumSyncTask = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
} on CanceledError {
|
} on CanceledError {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
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/constants/constants.dart';
|
||||||
@@ -11,7 +9,6 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
@@ -54,10 +51,9 @@ Future<void> syncCloudIds(ProviderContainer ref) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final assetApi = ref.read(apiServiceProvider).assetsApi;
|
final assetApi = ref.read(apiServiceProvider).assetsApi;
|
||||||
final cancellation = ref.read(cancellationProvider);
|
|
||||||
|
|
||||||
// Process cloud IDs in paginated batches
|
// Process cloud IDs in paginated batches
|
||||||
await _processCloudIdMappingsInBatches(db, currentUser.id, assetApi, canBulkUpdateMetadata, logger, cancellation);
|
await _processCloudIdMappingsInBatches(db, currentUser.id, assetApi, canBulkUpdateMetadata, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _processCloudIdMappingsInBatches(
|
Future<void> _processCloudIdMappingsInBatches(
|
||||||
@@ -66,17 +62,12 @@ Future<void> _processCloudIdMappingsInBatches(
|
|||||||
AssetsApi assetsApi,
|
AssetsApi assetsApi,
|
||||||
bool canBulkUpdate,
|
bool canBulkUpdate,
|
||||||
Logger logger,
|
Logger logger,
|
||||||
Completer<void> cancellation,
|
|
||||||
) async {
|
) async {
|
||||||
const pageSize = 20000;
|
const pageSize = 20000;
|
||||||
String? lastLocalId;
|
String? lastLocalId;
|
||||||
final seenRemoteAssetIds = <String>{};
|
final seenRemoteAssetIds = <String>{};
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (cancellation.isCompleted) {
|
|
||||||
logger.warning('Cloud ID migration cancelled. Stopping batch processing.');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
final mappings = await _fetchCloudIdMappings(drift, userId, pageSize, lastLocalId);
|
final mappings = await _fetchCloudIdMappings(drift, userId, pageSize, lastLocalId);
|
||||||
if (mappings.isEmpty) {
|
if (mappings.isEmpty) {
|
||||||
break;
|
break;
|
||||||
@@ -107,9 +98,9 @@ Future<void> _processCloudIdMappingsInBatches(
|
|||||||
|
|
||||||
if (items.isNotEmpty) {
|
if (items.isNotEmpty) {
|
||||||
if (canBulkUpdate) {
|
if (canBulkUpdate) {
|
||||||
await _bulkUpdateCloudIds(assetsApi, items, cancellation.future);
|
await _bulkUpdateCloudIds(assetsApi, items);
|
||||||
} else {
|
} else {
|
||||||
await _sequentialUpdateCloudIds(assetsApi, items, cancellation);
|
await _sequentialUpdateCloudIds(assetsApi, items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,35 +111,20 @@ Future<void> _processCloudIdMappingsInBatches(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sequentialUpdateCloudIds(
|
Future<void> _sequentialUpdateCloudIds(AssetsApi assetsApi, List<AssetMetadataBulkUpsertItemDto> items) async {
|
||||||
AssetsApi assetsApi,
|
|
||||||
List<AssetMetadataBulkUpsertItemDto> items,
|
|
||||||
Completer<void> cancellation,
|
|
||||||
) async {
|
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
if (cancellation.isCompleted) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
final upsertItem = AssetMetadataUpsertItemDto(key: item.key, value: item.value);
|
final upsertItem = AssetMetadataUpsertItemDto(key: item.key, value: item.value);
|
||||||
try {
|
try {
|
||||||
await assetsApi.updateAssetMetadata(
|
await assetsApi.updateAssetMetadata(item.assetId, AssetMetadataUpsertDto(items: [upsertItem]));
|
||||||
item.assetId,
|
|
||||||
AssetMetadataUpsertDto(items: [upsertItem]),
|
|
||||||
abortTrigger: cancellation.future,
|
|
||||||
);
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
Logger('migrateCloudIds').warning('Failed to update metadata for asset ${item.assetId}', error, stack);
|
Logger('migrateCloudIds').warning('Failed to update metadata for asset ${item.assetId}', error, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _bulkUpdateCloudIds(
|
Future<void> _bulkUpdateCloudIds(AssetsApi assetsApi, List<AssetMetadataBulkUpsertItemDto> items) async {
|
||||||
AssetsApi assetsApi,
|
|
||||||
List<AssetMetadataBulkUpsertItemDto> items,
|
|
||||||
Future<void> abortTrigger,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
await assetsApi.updateBulkAssetMetadata(AssetMetadataBulkUpsertDto(items: items), abortTrigger: abortTrigger);
|
await assetsApi.updateBulkAssetMetadata(AssetMetadataBulkUpsertDto(items: items));
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
Logger('migrateCloudIds').warning('Failed to bulk update metadata', error, stack);
|
Logger('migrateCloudIds').warning('Failed to bulk update metadata', error, stack);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ extension DTOToAsset on api.AssetResponseDto {
|
|||||||
height: height?.toInt(),
|
height: height?.toInt(),
|
||||||
width: width?.toInt(),
|
width: width?.toInt(),
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
livePhotoVideoId: livePhotoVideoId.orElse(null),
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
thumbHash: thumbhash,
|
thumbHash: thumbhash,
|
||||||
localId: null,
|
localId: null,
|
||||||
type: type.toAssetType(),
|
type: type.toAssetType(),
|
||||||
stackId: stack.orElse(null)?.id,
|
stackId: stack?.id,
|
||||||
isEdited: isEdited,
|
isEdited: isEdited,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -41,13 +41,13 @@ extension DTOToAsset on api.AssetResponseDto {
|
|||||||
height: height?.toInt(),
|
height: height?.toInt(),
|
||||||
width: width?.toInt(),
|
width: width?.toInt(),
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
livePhotoVideoId: livePhotoVideoId.orElse(null),
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
thumbHash: thumbhash,
|
thumbHash: thumbhash,
|
||||||
localId: null,
|
localId: null,
|
||||||
type: type.toAssetType(),
|
type: type.toAssetType(),
|
||||||
stackId: stack.orElse(null)?.id,
|
stackId: stack?.id,
|
||||||
isEdited: isEdited,
|
isEdited: isEdited,
|
||||||
exifInfo: exifInfo.orElse(null) != null ? ExifDtoConverter.fromDto(exifInfo.orElse(null)!) : const ExifInfo(),
|
exifInfo: exifInfo != null ? ExifDtoConverter.fromDto(exifInfo!) : const ExifInfo(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|||||||
|
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
|
||||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)')
|
|
||||||
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
const LocalAssetEntity();
|
const LocalAssetEntity();
|
||||||
|
|
||||||
|
|||||||
@@ -1348,7 +1348,3 @@ i0.Index get idxLocalAssetCloudId => i0.Index(
|
|||||||
'idx_local_asset_cloud_id',
|
'idx_local_asset_cloud_id',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
);
|
);
|
||||||
i0.Index get idxLocalAssetCreatedAt => i0.Index(
|
|
||||||
'idx_local_asset_created_at',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class Drift extends $Drift {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 28;
|
int get schemaVersion => 27;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -279,9 +279,6 @@ class Drift extends $Drift {
|
|||||||
from26To27: (m, v27) async {
|
from26To27: (m, v27) async {
|
||||||
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
||||||
},
|
},
|
||||||
from27To28: (m, v28) async {
|
|
||||||
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i7.idxLocalAlbumAssetAlbumAsset,
|
i7.idxLocalAlbumAssetAlbumAsset,
|
||||||
i4.idxLocalAssetChecksum,
|
i4.idxLocalAssetChecksum,
|
||||||
i4.idxLocalAssetCloudId,
|
i4.idxLocalAssetCloudId,
|
||||||
i4.idxLocalAssetCreatedAt,
|
|
||||||
i3.idxStackPrimaryAssetId,
|
i3.idxStackPrimaryAssetId,
|
||||||
i2.uQRemoteAssetsOwnerChecksum,
|
i2.uQRemoteAssetsOwnerChecksum,
|
||||||
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
|||||||
@@ -14083,554 +14083,6 @@ final class Schema27 extends i0.VersionedSchema {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Schema28 extends i0.VersionedSchema {
|
|
||||||
Schema28({required super.database}) : super(version: 28);
|
|
||||||
@override
|
|
||||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
|
||||||
userEntity,
|
|
||||||
remoteAssetEntity,
|
|
||||||
stackEntity,
|
|
||||||
localAssetEntity,
|
|
||||||
remoteAlbumEntity,
|
|
||||||
localAlbumEntity,
|
|
||||||
localAlbumAssetEntity,
|
|
||||||
idxLocalAlbumAssetAlbumAsset,
|
|
||||||
idxLocalAssetChecksum,
|
|
||||||
idxLocalAssetCloudId,
|
|
||||||
idxLocalAssetCreatedAt,
|
|
||||||
idxStackPrimaryAssetId,
|
|
||||||
uQRemoteAssetsOwnerChecksum,
|
|
||||||
uQRemoteAssetsOwnerLibraryChecksum,
|
|
||||||
idxRemoteAssetChecksum,
|
|
||||||
idxRemoteAssetStackId,
|
|
||||||
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
|
||||||
authUserEntity,
|
|
||||||
userMetadataEntity,
|
|
||||||
partnerEntity,
|
|
||||||
remoteExifEntity,
|
|
||||||
remoteAlbumAssetEntity,
|
|
||||||
remoteAlbumUserEntity,
|
|
||||||
remoteAssetCloudIdEntity,
|
|
||||||
memoryEntity,
|
|
||||||
memoryAssetEntity,
|
|
||||||
personEntity,
|
|
||||||
assetFaceEntity,
|
|
||||||
storeEntity,
|
|
||||||
trashedLocalAssetEntity,
|
|
||||||
assetEditEntity,
|
|
||||||
settings,
|
|
||||||
idxPartnerSharedWithId,
|
|
||||||
idxLatLng,
|
|
||||||
idxRemoteExifCity,
|
|
||||||
idxRemoteAlbumAssetAlbumAsset,
|
|
||||||
idxRemoteAssetCloudId,
|
|
||||||
idxPersonOwnerId,
|
|
||||||
idxAssetFacePersonId,
|
|
||||||
idxAssetFaceAssetId,
|
|
||||||
idxAssetFaceVisiblePerson,
|
|
||||||
idxTrashedLocalAssetChecksum,
|
|
||||||
idxTrashedLocalAssetAlbum,
|
|
||||||
idxAssetEditAssetId,
|
|
||||||
];
|
|
||||||
late final Shape33 userEntity = Shape33(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'user_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_108,
|
|
||||||
_column_109,
|
|
||||||
_column_110,
|
|
||||||
_column_111,
|
|
||||||
_column_112,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape50 remoteAssetEntity = Shape50(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'remote_asset_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_108,
|
|
||||||
_column_113,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_116,
|
|
||||||
_column_117,
|
|
||||||
_column_118,
|
|
||||||
_column_107,
|
|
||||||
_column_119,
|
|
||||||
_column_120,
|
|
||||||
_column_121,
|
|
||||||
_column_122,
|
|
||||||
_column_123,
|
|
||||||
_column_124,
|
|
||||||
_column_212,
|
|
||||||
_column_125,
|
|
||||||
_column_126,
|
|
||||||
_column_127,
|
|
||||||
_column_128,
|
|
||||||
_column_129,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape35 stackEntity = Shape35(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'stack_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_121,
|
|
||||||
_column_130,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape36 localAssetEntity = Shape36(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'local_asset_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_108,
|
|
||||||
_column_113,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_116,
|
|
||||||
_column_117,
|
|
||||||
_column_118,
|
|
||||||
_column_107,
|
|
||||||
_column_131,
|
|
||||||
_column_120,
|
|
||||||
_column_132,
|
|
||||||
_column_133,
|
|
||||||
_column_134,
|
|
||||||
_column_135,
|
|
||||||
_column_136,
|
|
||||||
_column_137,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape48 remoteAlbumEntity = Shape48(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'remote_album_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_108,
|
|
||||||
_column_138,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_139,
|
|
||||||
_column_140,
|
|
||||||
_column_141,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape38 localAlbumEntity = Shape38(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'local_album_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_108,
|
|
||||||
_column_115,
|
|
||||||
_column_142,
|
|
||||||
_column_143,
|
|
||||||
_column_144,
|
|
||||||
_column_145,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape39 localAlbumAssetEntity = Shape39(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'local_album_asset_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
|
||||||
columns: [_column_146, _column_147, _column_145],
|
|
||||||
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 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 idxLocalAssetCreatedAt = i1.Index(
|
|
||||||
'idx_local_asset_created_at',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
|
||||||
);
|
|
||||||
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 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 idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
|
||||||
'idx_remote_asset_owner_visibility_deleted_created',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
|
||||||
);
|
|
||||||
late final Shape40 authUserEntity = Shape40(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'auth_user_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_108,
|
|
||||||
_column_109,
|
|
||||||
_column_148,
|
|
||||||
_column_110,
|
|
||||||
_column_111,
|
|
||||||
_column_149,
|
|
||||||
_column_150,
|
|
||||||
_column_151,
|
|
||||||
_column_152,
|
|
||||||
],
|
|
||||||
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_153, _column_154, _column_155],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape41 partnerEntity = Shape41(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'partner_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
|
||||||
columns: [_column_156, _column_157, _column_158],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape42 remoteExifEntity = Shape42(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'remote_exif_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
|
||||||
columns: [
|
|
||||||
_column_159,
|
|
||||||
_column_160,
|
|
||||||
_column_161,
|
|
||||||
_column_162,
|
|
||||||
_column_163,
|
|
||||||
_column_164,
|
|
||||||
_column_117,
|
|
||||||
_column_116,
|
|
||||||
_column_165,
|
|
||||||
_column_166,
|
|
||||||
_column_167,
|
|
||||||
_column_168,
|
|
||||||
_column_135,
|
|
||||||
_column_136,
|
|
||||||
_column_169,
|
|
||||||
_column_170,
|
|
||||||
_column_171,
|
|
||||||
_column_172,
|
|
||||||
_column_173,
|
|
||||||
_column_174,
|
|
||||||
_column_175,
|
|
||||||
_column_176,
|
|
||||||
],
|
|
||||||
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_159, _column_177],
|
|
||||||
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_177, _column_153, _column_178],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'remote_asset_cloud_id_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
|
||||||
columns: [
|
|
||||||
_column_159,
|
|
||||||
_column_179,
|
|
||||||
_column_180,
|
|
||||||
_column_134,
|
|
||||||
_column_135,
|
|
||||||
_column_136,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape44 memoryEntity = Shape44(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'memory_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_124,
|
|
||||||
_column_121,
|
|
||||||
_column_113,
|
|
||||||
_column_181,
|
|
||||||
_column_182,
|
|
||||||
_column_183,
|
|
||||||
_column_184,
|
|
||||||
_column_185,
|
|
||||||
_column_186,
|
|
||||||
],
|
|
||||||
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_159, _column_187],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape45 personEntity = Shape45(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'person_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_121,
|
|
||||||
_column_108,
|
|
||||||
_column_188,
|
|
||||||
_column_189,
|
|
||||||
_column_190,
|
|
||||||
_column_191,
|
|
||||||
_column_192,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape46 assetFaceEntity = Shape46(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'asset_face_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_159,
|
|
||||||
_column_193,
|
|
||||||
_column_194,
|
|
||||||
_column_195,
|
|
||||||
_column_196,
|
|
||||||
_column_197,
|
|
||||||
_column_198,
|
|
||||||
_column_199,
|
|
||||||
_column_200,
|
|
||||||
_column_201,
|
|
||||||
_column_124,
|
|
||||||
],
|
|
||||||
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_202, _column_203, _column_204],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape47 trashedLocalAssetEntity = Shape47(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'trashed_local_asset_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
|
||||||
columns: [
|
|
||||||
_column_108,
|
|
||||||
_column_113,
|
|
||||||
_column_114,
|
|
||||||
_column_115,
|
|
||||||
_column_116,
|
|
||||||
_column_117,
|
|
||||||
_column_118,
|
|
||||||
_column_107,
|
|
||||||
_column_205,
|
|
||||||
_column_131,
|
|
||||||
_column_120,
|
|
||||||
_column_132,
|
|
||||||
_column_206,
|
|
||||||
_column_137,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape32 assetEditEntity = Shape32(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'asset_edit_entity',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY(id)'],
|
|
||||||
columns: [
|
|
||||||
_column_107,
|
|
||||||
_column_159,
|
|
||||||
_column_207,
|
|
||||||
_column_208,
|
|
||||||
_column_209,
|
|
||||||
],
|
|
||||||
attachedDatabase: database,
|
|
||||||
),
|
|
||||||
alias: null,
|
|
||||||
);
|
|
||||||
late final Shape49 settings = Shape49(
|
|
||||||
source: i0.VersionedTable(
|
|
||||||
entityName: 'settings',
|
|
||||||
withoutRowId: true,
|
|
||||||
isStrict: true,
|
|
||||||
tableConstraints: ['PRIMARY KEY("key")'],
|
|
||||||
columns: [_column_210, _column_211, _column_115],
|
|
||||||
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 idxRemoteExifCity = i1.Index(
|
|
||||||
'idx_remote_exif_city',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
|
||||||
);
|
|
||||||
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 idxAssetFaceVisiblePerson = i1.Index(
|
|
||||||
'idx_asset_face_visible_person',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
|
||||||
);
|
|
||||||
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)',
|
|
||||||
);
|
|
||||||
final i1.Index idxAssetEditAssetId = i1.Index(
|
|
||||||
'idx_asset_edit_asset_id',
|
|
||||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
||||||
@@ -14658,7 +14110,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
||||||
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
|
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
|
||||||
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
|
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
|
||||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -14792,11 +14243,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from26To27(migrator, schema);
|
await from26To27(migrator, schema);
|
||||||
return 27;
|
return 27;
|
||||||
case 27:
|
|
||||||
final schema = Schema28(database: database);
|
|
||||||
final migrator = i1.Migrator(database, schema);
|
|
||||||
await from27To28(migrator, schema);
|
|
||||||
return 28;
|
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -14830,7 +14276,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
|
||||||
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
|
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
|
||||||
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
|
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
|
||||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -14859,6 +14304,5 @@ i1.OnUpgrade stepByStep({
|
|||||||
from24To25: from24To25,
|
from24To25: from24To25,
|
||||||
from25To26: from25To26,
|
from25To26: from25To26,
|
||||||
from26To27: from26To27,
|
from26To27: from26To27,
|
||||||
from27To28: from27To28,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
||||||
])
|
])
|
||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAssetEntity.checksum.isNull())
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAssetEntity.checksum.isNull())
|
||||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]);
|
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
|
||||||
|
|
||||||
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,7 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
|||||||
condition = condition & _db.remoteAssetEntity.isFavorite.equals(true);
|
condition = condition & _db.remoteAssetEntity.isFavorite.equals(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final timeRange = options.timeRange;
|
if (options.relativeDays != 0) {
|
||||||
final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome;
|
|
||||||
|
|
||||||
if (hasCustomRange) {
|
|
||||||
timeRange.from.ifPresent((from) {
|
|
||||||
condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from);
|
|
||||||
});
|
|
||||||
|
|
||||||
timeRange.to.ifPresent((to) {
|
|
||||||
condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to);
|
|
||||||
});
|
|
||||||
} else if (options.relativeDays > 0) {
|
|
||||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||||
condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate);
|
condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,64 +20,50 @@ class SearchApiRepository extends ApiRepository {
|
|||||||
(filter.assetId != null && filter.assetId!.isNotEmpty)) {
|
(filter.assetId != null && filter.assetId!.isNotEmpty)) {
|
||||||
return _api.searchSmart(
|
return _api.searchSmart(
|
||||||
SmartSearchDto(
|
SmartSearchDto(
|
||||||
query: filter.context == null ? const Optional.absent() : Optional.present(filter.context!),
|
query: filter.context,
|
||||||
queryAssetId: filter.assetId == null ? const Optional.absent() : Optional.present(filter.assetId!),
|
queryAssetId: filter.assetId,
|
||||||
language: filter.language == null ? const Optional.absent() : Optional.present(filter.language!),
|
language: filter.language,
|
||||||
country: filter.location.country == null
|
country: filter.location.country,
|
||||||
? const Optional.absent()
|
state: filter.location.state,
|
||||||
: Optional.present(filter.location.country!),
|
city: filter.location.city,
|
||||||
state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!),
|
make: filter.camera.make,
|
||||||
city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!),
|
model: filter.camera.model,
|
||||||
make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!),
|
takenAfter: filter.date.takenAfter,
|
||||||
model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!),
|
takenBefore: filter.date.takenBefore,
|
||||||
takenAfter: filter.date.takenAfter == null
|
visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline,
|
||||||
? const Optional.absent()
|
rating: filter.rating.rating,
|
||||||
: Optional.present(filter.date.takenAfter!),
|
isFavorite: filter.display.isFavorite ? true : null,
|
||||||
takenBefore: filter.date.takenBefore == null
|
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||||
? const Optional.absent()
|
personIds: filter.people.map((e) => e.id).toList(),
|
||||||
: Optional.present(filter.date.takenBefore!),
|
tagIds: filter.tagIds,
|
||||||
visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline),
|
type: type,
|
||||||
rating: filter.rating.rating == null ? const Optional.absent() : Optional.present(filter.rating.rating!),
|
page: page,
|
||||||
isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(),
|
size: 100,
|
||||||
isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(),
|
|
||||||
personIds: Optional.present(filter.people.map((e) => e.id).toList()),
|
|
||||||
tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!),
|
|
||||||
type: type == null ? const Optional.absent() : Optional.present(type),
|
|
||||||
page: Optional.present(page),
|
|
||||||
size: const Optional.present(100),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _api.searchAssets(
|
return _api.searchAssets(
|
||||||
MetadataSearchDto(
|
MetadataSearchDto(
|
||||||
originalFileName: filter.filename != null && filter.filename!.isNotEmpty
|
originalFileName: filter.filename != null && filter.filename!.isNotEmpty ? filter.filename : null,
|
||||||
? Optional.present(filter.filename!)
|
country: filter.location.country,
|
||||||
: const Optional.absent(),
|
description: filter.description != null && filter.description!.isNotEmpty ? filter.description : null,
|
||||||
country: filter.location.country == null ? const Optional.absent() : Optional.present(filter.location.country!),
|
ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? filter.ocr : null,
|
||||||
description: filter.description != null && filter.description!.isNotEmpty
|
state: filter.location.state,
|
||||||
? Optional.present(filter.description!)
|
city: filter.location.city,
|
||||||
: const Optional.absent(),
|
make: filter.camera.make,
|
||||||
ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? Optional.present(filter.ocr!) : const Optional.absent(),
|
model: filter.camera.model,
|
||||||
state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!),
|
takenAfter: filter.date.takenAfter,
|
||||||
city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!),
|
takenBefore: filter.date.takenBefore,
|
||||||
make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!),
|
visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline,
|
||||||
model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!),
|
rating: filter.rating.rating,
|
||||||
takenAfter: filter.date.takenAfter == null
|
isFavorite: filter.display.isFavorite ? true : null,
|
||||||
? const Optional.absent()
|
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
|
||||||
: Optional.present(filter.date.takenAfter!),
|
personIds: filter.people.map((e) => e.id).toList(),
|
||||||
takenBefore: filter.date.takenBefore == null
|
tagIds: filter.tagIds,
|
||||||
? const Optional.absent()
|
type: type,
|
||||||
: Optional.present(filter.date.takenBefore!),
|
page: page,
|
||||||
visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline),
|
size: 1000,
|
||||||
rating: filter.rating.rating == null ? const Optional.absent() : Optional.present(filter.rating.rating!),
|
|
||||||
isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(),
|
|
||||||
isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(),
|
|
||||||
personIds: Optional.present(filter.people.map((e) => e.id).toList()),
|
|
||||||
tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!),
|
|
||||||
type: type == null ? const Optional.absent() : Optional.present(type),
|
|
||||||
page: Optional.present(page),
|
|
||||||
size: const Optional.present(1000),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class SyncApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteSyncAck(List<SyncEntityType> types) {
|
Future<void> deleteSyncAck(List<SyncEntityType> types) {
|
||||||
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: Optional.present(types)));
|
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: types));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> streamChanges(
|
Future<void> streamChanges(
|
||||||
@@ -29,7 +29,6 @@ class SyncApiRepository {
|
|||||||
Function()? onReset,
|
Function()? onReset,
|
||||||
int batchSize = kSyncEventBatchSize,
|
int batchSize = kSyncEventBatchSize,
|
||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
Future<void>? abortSignal,
|
|
||||||
}) async {
|
}) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
final client = httpClient ?? NetworkRepository.client;
|
final client = httpClient ?? NetworkRepository.client;
|
||||||
@@ -37,7 +36,7 @@ class SyncApiRepository {
|
|||||||
|
|
||||||
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
|
||||||
|
|
||||||
final request = http.AbortableRequest('POST', Uri.parse(endpoint), abortTrigger: abortSignal);
|
final request = http.Request('POST', Uri.parse(endpoint));
|
||||||
request.headers.addAll(headers);
|
request.headers.addAll(headers);
|
||||||
request.body = jsonEncode(
|
request.body = jsonEncode(
|
||||||
SyncStreamDto(
|
SyncStreamDto(
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
email: Value(user.email),
|
email: Value(user.email),
|
||||||
hasProfileImage: Value(user.hasProfileImage),
|
hasProfileImage: Value(user.hasProfileImage),
|
||||||
profileChangedAt: Value(user.profileChangedAt),
|
profileChangedAt: Value(user.profileChangedAt),
|
||||||
avatarColor: Value(user.avatarColor.orElse(null)?.toAvatarColor() ?? AvatarColor.primary),
|
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
|
||||||
isAdmin: Value(user.isAdmin),
|
isAdmin: Value(user.isAdmin),
|
||||||
pinCode: Value(user.pinCode),
|
pinCode: Value(user.pinCode),
|
||||||
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
|
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
|
||||||
@@ -133,7 +133,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
email: Value(user.email),
|
email: Value(user.email),
|
||||||
hasProfileImage: Value(user.hasProfileImage),
|
hasProfileImage: Value(user.hasProfileImage),
|
||||||
profileChangedAt: Value(user.profileChangedAt),
|
profileChangedAt: Value(user.profileChangedAt),
|
||||||
avatarColor: Value(user.avatarColor.orElse(null)?.toAvatarColor() ?? AvatarColor.primary),
|
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
|
||||||
);
|
);
|
||||||
|
|
||||||
batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion));
|
batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion));
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.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/time_range.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/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
@@ -22,7 +21,6 @@ class TimelineMapOptions {
|
|||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
final int relativeDays;
|
final int relativeDays;
|
||||||
final TimeRange timeRange;
|
|
||||||
|
|
||||||
const TimelineMapOptions({
|
const TimelineMapOptions({
|
||||||
required this.bounds,
|
required this.bounds,
|
||||||
@@ -30,7 +28,6 @@ class TimelineMapOptions {
|
|||||||
this.includeArchived = false,
|
this.includeArchived = false,
|
||||||
this.withPartners = false,
|
this.withPartners = false,
|
||||||
this.relativeDays = 0,
|
this.relativeDays = 0,
|
||||||
this.timeRange = const TimeRange(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,21 +553,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
final timeRange = options.timeRange;
|
if (options.relativeDays != 0) {
|
||||||
|
|
||||||
final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome;
|
|
||||||
|
|
||||||
if (hasCustomRange) {
|
|
||||||
timeRange.from.ifPresent((from) {
|
|
||||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from));
|
|
||||||
});
|
|
||||||
|
|
||||||
timeRange.to.ifPresent((to) {
|
|
||||||
query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to));
|
|
||||||
});
|
|
||||||
} else if (options.relativeDays > 0) {
|
|
||||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||||
|
|
||||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,21 +595,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
query.where(_db.remoteAssetEntity.isFavorite.equals(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
final timeRange = options.timeRange;
|
if (options.relativeDays != 0) {
|
||||||
|
|
||||||
final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome;
|
|
||||||
|
|
||||||
if (hasCustomRange) {
|
|
||||||
timeRange.from.ifPresent((from) {
|
|
||||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from));
|
|
||||||
});
|
|
||||||
|
|
||||||
timeRange.to.ifPresent((to) {
|
|
||||||
query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to));
|
|
||||||
});
|
|
||||||
} else if (options.relativeDays > 0) {
|
|
||||||
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays));
|
||||||
|
|
||||||
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ import 'package:openapi/api.dart';
|
|||||||
abstract final class ExifDtoConverter {
|
abstract final class ExifDtoConverter {
|
||||||
static ExifInfo fromDto(ExifResponseDto dto) {
|
static ExifInfo fromDto(ExifResponseDto dto) {
|
||||||
return ExifInfo(
|
return ExifInfo(
|
||||||
fileSize: dto.fileSizeInByte.orElse(null),
|
fileSize: dto.fileSizeInByte,
|
||||||
description: dto.description.orElse(null),
|
description: dto.description,
|
||||||
orientation: dto.orientation.orElse(null),
|
orientation: dto.orientation,
|
||||||
timeZone: dto.timeZone.orElse(null),
|
timeZone: dto.timeZone,
|
||||||
dateTimeOriginal: dto.dateTimeOriginal.orElse(null),
|
dateTimeOriginal: dto.dateTimeOriginal,
|
||||||
isFlipped: isOrientationFlipped(dto.orientation.orElse(null)),
|
isFlipped: isOrientationFlipped(dto.orientation),
|
||||||
latitude: dto.latitude.orElse(null)?.toDouble(),
|
latitude: dto.latitude?.toDouble(),
|
||||||
longitude: dto.longitude.orElse(null)?.toDouble(),
|
longitude: dto.longitude?.toDouble(),
|
||||||
city: dto.city.orElse(null),
|
city: dto.city,
|
||||||
state: dto.state.orElse(null),
|
state: dto.state,
|
||||||
country: dto.country.orElse(null),
|
country: dto.country,
|
||||||
make: dto.make.orElse(null),
|
make: dto.make,
|
||||||
model: dto.model.orElse(null),
|
model: dto.model,
|
||||||
lens: dto.lensModel.orElse(null),
|
lens: dto.lensModel,
|
||||||
f: dto.fNumber.orElse(null)?.toDouble(),
|
f: dto.fNumber?.toDouble(),
|
||||||
mm: dto.focalLength.orElse(null)?.toDouble(),
|
mm: dto.focalLength?.toDouble(),
|
||||||
iso: dto.iso.orElse(null)?.toInt(),
|
iso: dto.iso?.toInt(),
|
||||||
exposureSeconds: exposureTimeToSeconds(dto.exposureTime.orElse(null)),
|
exposureSeconds: exposureTimeToSeconds(dto.exposureTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ abstract final class UserConverter {
|
|||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
avatarColor: dto.avatarColor.toAvatarColor(),
|
avatarColor: dto.avatarColor.toAvatarColor(),
|
||||||
memoryEnabled: false,
|
memoryEnabled: false,
|
||||||
inTimeline: dto.inTimeline.orElse(null) ?? false,
|
inTimeline: dto.inTimeline ?? false,
|
||||||
isPartnerSharedBy: false,
|
isPartnerSharedBy: false,
|
||||||
isPartnerSharedWith: false,
|
isPartnerSharedWith: false,
|
||||||
profileChangedAt: dto.profileChangedAt,
|
profileChangedAt: dto.profileChangedAt,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
|||||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
@@ -129,7 +128,6 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
dPrint(() => "[APP STATE] resumed");
|
dPrint(() => "[APP STATE] resumed");
|
||||||
ref.read(appStateProvider.notifier).handleAppResume();
|
ref.read(appStateProvider.notifier).handleAppResume();
|
||||||
unawaited(ref.read(viewIntentHandlerProvider).onAppResumed());
|
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
dPrint(() => "[APP STATE] inactive");
|
dPrint(() => "[APP STATE] inactive");
|
||||||
@@ -235,7 +233,6 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.read(viewIntentHandlerProvider).init();
|
|
||||||
ref.read(shareIntentUploadProvider.notifier).init();
|
ref.read(shareIntentUploadProvider.notifier).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ class SharedLink {
|
|||||||
slug = dto.slug,
|
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.orElse(null)?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
|
||||||
: "INDIVIDUAL SHARE",
|
: "INDIVIDUAL SHARE",
|
||||||
thumbAssetId = dto.type == SharedLinkType.ALBUM
|
thumbAssetId = dto.type == SharedLinkType.ALBUM
|
||||||
? dto.album.orElse(null)?.albumThumbnailAssetId
|
? dto.album?.albumThumbnailAssetId
|
||||||
: dto.assets.isNotEmpty
|
: dto.assets.isNotEmpty
|
||||||
? dto.assets[0].id
|
? dto.assets[0].id
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
extension ViewIntentPayloadX on ViewIntentPayload {
|
|
||||||
String get fileName {
|
|
||||||
final resolvedPath = path;
|
|
||||||
if (resolvedPath != null && resolvedPath.isNotEmpty) {
|
|
||||||
return basename(resolvedPath);
|
|
||||||
}
|
|
||||||
return localAssetId ?? 'view_intent_asset';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isImage => mimeType.toLowerCase().startsWith('image/');
|
|
||||||
|
|
||||||
bool get isVideo => mimeType.toLowerCase().startsWith('video/');
|
|
||||||
|
|
||||||
AssetPlaybackStyle get playbackStyle {
|
|
||||||
if (isVideo) {
|
|
||||||
return AssetPlaybackStyle.video;
|
|
||||||
}
|
|
||||||
|
|
||||||
final normalizedMimeType = mimeType.toLowerCase();
|
|
||||||
if (normalizedMimeType == 'image/gif' || normalizedMimeType == 'image/webp') {
|
|
||||||
return AssetPlaybackStyle.imageAnimated;
|
|
||||||
}
|
|
||||||
|
|
||||||
final normalizedPath = path?.toLowerCase();
|
|
||||||
if (normalizedPath != null && (normalizedPath.endsWith('.gif') || normalizedPath.endsWith('.webp'))) {
|
|
||||||
return AssetPlaybackStyle.imageAnimated;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AssetPlaybackStyle.image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
@@ -16,16 +15,11 @@ import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.w
|
|||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/permission.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@@ -168,7 +162,11 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
const _BackupFooter(),
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.info_outline_rounded),
|
||||||
|
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
|
||||||
|
label: Text("view_details".t(context: context)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -179,137 +177,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BackupFooter extends ConsumerStatefulWidget {
|
|
||||||
const _BackupFooter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<_BackupFooter> createState() => _BackupFooterState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindingObserver {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
if (CurrentPlatform.isAndroid && state == AppLifecycleState.resumed && mounted) {
|
|
||||||
unawaited(ref.read(notificationPermissionProvider.notifier).getNotificationPermission());
|
|
||||||
unawaited(ref.read(batteryOptimizationProvider.notifier).getBatteryOptimizationPermission());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showPermissionsDialog() {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) => AlertDialog(
|
|
||||||
content: Text(context.t.notification_permission_dialog_content),
|
|
||||||
actions: [
|
|
||||||
ImmichTextButton(
|
|
||||||
labelText: context.t.cancel,
|
|
||||||
variant: .ghost,
|
|
||||||
expanded: false,
|
|
||||||
onPressed: () => ContextHelper(ctx).pop(),
|
|
||||||
),
|
|
||||||
ImmichTextButton(
|
|
||||||
labelText: context.t.settings,
|
|
||||||
variant: .ghost,
|
|
||||||
expanded: false,
|
|
||||||
onPressed: () {
|
|
||||||
ContextHelper(context).pop();
|
|
||||||
openAppSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showBatteryOptimizationInfo() {
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(context.t.backup_controller_page_background_battery_info_title),
|
|
||||||
content: SingleChildScrollView(child: Text(context.t.backup_controller_page_background_battery_info_message)),
|
|
||||||
actions: [
|
|
||||||
ImmichTextButton(
|
|
||||||
labelText: context.t.backup_controller_page_background_battery_info_link,
|
|
||||||
variant: .ghost,
|
|
||||||
expanded: false,
|
|
||||||
onPressed: () => launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication),
|
|
||||||
),
|
|
||||||
ImmichTextButton(
|
|
||||||
labelText: context.t.backup_controller_page_background_battery_info_ok,
|
|
||||||
variant: .ghost,
|
|
||||||
expanded: false,
|
|
||||||
onPressed: () => ContextHelper(ctx).pop(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final isBackupEnabled = ref.watch(appConfigProvider.select((config) => config.backup.enabled));
|
|
||||||
final notificationStatus = ref.watch(notificationPermissionProvider);
|
|
||||||
final batteryOptimizationStatus = ref.watch(batteryOptimizationProvider).valueOrNull;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
if (CurrentPlatform.isAndroid && isBackupEnabled) ...[
|
|
||||||
if (notificationStatus != PermissionStatus.granted)
|
|
||||||
TextButton.icon(
|
|
||||||
iconAlignment: .end,
|
|
||||||
icon: Icon(Icons.open_in_new_outlined, color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
label: Text(
|
|
||||||
context.t.notification_backup_reliability,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) {
|
|
||||||
if (p == PermissionStatus.permanentlyDenied) {
|
|
||||||
showPermissionsDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (notificationStatus != PermissionStatus.granted && batteryOptimizationStatus != PermissionStatus.granted)
|
|
||||||
const Divider(indent: 32, endIndent: 32),
|
|
||||||
if (batteryOptimizationStatus != PermissionStatus.granted)
|
|
||||||
TextButton.icon(
|
|
||||||
iconAlignment: .end,
|
|
||||||
icon: Icon(Icons.open_in_new_outlined, color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
label: Text(
|
|
||||||
context.t.battery_optimization_backup_reliability,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
onPressed: showBatteryOptimizationInfo,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
TextButton.icon(
|
|
||||||
icon: const Icon(Icons.info_outline_rounded),
|
|
||||||
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
|
|
||||||
label: Text(context.t.view_details),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BackupAlbumSelectionCard extends ConsumerWidget {
|
class _BackupAlbumSelectionCard extends ConsumerWidget {
|
||||||
const _BackupAlbumSelectionCard();
|
const _BackupAlbumSelectionCard();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import 'package:immich_mobile/providers/auth.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||||
@@ -315,7 +314,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
final wsProvider = ref.read(websocketProvider.notifier);
|
final wsProvider = ref.read(websocketProvider.notifier);
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||||
final viewIntentHandler = ref.read(viewIntentHandlerProvider);
|
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||||
@@ -330,8 +328,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await viewIntentHandler.flushDeferredViewIntent();
|
|
||||||
|
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
backgroundManager.hashAssets().then((_) {
|
backgroundManager.hashAssets().then((_) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
|||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
||||||
import 'package:immich_mobile/services/shared_link.service.dart';
|
import 'package:immich_mobile/services/shared_link.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@@ -366,10 +365,11 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
bool? download;
|
bool? download;
|
||||||
bool? upload;
|
bool? upload;
|
||||||
bool? meta;
|
bool? meta;
|
||||||
var password = const Optional<String?>.absent();
|
String? desc;
|
||||||
var description = const Optional<String?>.absent();
|
String? password;
|
||||||
String? slug;
|
String? slug;
|
||||||
var expiry = const Optional<DateTime?>.absent();
|
DateTime? expiry;
|
||||||
|
bool? changeExpiry;
|
||||||
|
|
||||||
if (allowDownload.value != existingLink!.allowDownload) {
|
if (allowDownload.value != existingLink!.allowDownload) {
|
||||||
download = allowDownload.value;
|
download = allowDownload.value;
|
||||||
@@ -383,16 +383,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
meta = showMetadata.value;
|
meta = showMetadata.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (descriptionController.text != (existingLink!.description ?? '')) {
|
if (descriptionController.text != existingLink!.description) {
|
||||||
description = descriptionController.text.isEmpty
|
desc = descriptionController.text;
|
||||||
? const Optional.present(null)
|
|
||||||
: Optional.present(descriptionController.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordController.text != (existingLink!.password ?? '')) {
|
if (passwordController.text != existingLink!.password) {
|
||||||
password = passwordController.text.isEmpty
|
password = passwordController.text;
|
||||||
? const Optional.present(null)
|
|
||||||
: Optional.present(passwordController.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slugController.text != (existingLink!.slug ?? "")) {
|
if (slugController.text != (existingLink!.slug ?? "")) {
|
||||||
@@ -403,7 +399,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final newExpiry = expiryAfter.value;
|
final newExpiry = expiryAfter.value;
|
||||||
if (newExpiry?.toUtc() != existingLink!.expiresAt?.toUtc()) {
|
if (newExpiry?.toUtc() != existingLink!.expiresAt?.toUtc()) {
|
||||||
expiry = newExpiry == null ? const Optional.present(null) : Optional.present(newExpiry.toUtc());
|
expiry = newExpiry;
|
||||||
|
changeExpiry = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
@@ -413,10 +410,11 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
showMeta: meta,
|
showMeta: meta,
|
||||||
allowDownload: download,
|
allowDownload: download,
|
||||||
allowUpload: upload,
|
allowUpload: upload,
|
||||||
description: description,
|
description: desc,
|
||||||
password: password,
|
password: password,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
expiresAt: expiry,
|
expiresAt: expiry?.toUtc(),
|
||||||
|
changeExpiry: changeExpiry,
|
||||||
);
|
);
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
-14
@@ -635,20 +635,6 @@ class NativeSyncApi {
|
|||||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancelSync() async {
|
|
||||||
final pigeonVar_channelName =
|
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
|
||||||
);
|
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
|
||||||
|
|
||||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, List<PlatformAsset>>> getTrashedAssets() async {
|
Future<Map<String, List<PlatformAsset>>> getTrashedAssets() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix';
|
||||||
|
|||||||
-27
@@ -26,8 +26,6 @@ Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName,
|
|||||||
return replyList.firstOrNull;
|
return replyList.firstOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PermissionStatus { granted, denied, permanentlyDenied }
|
|
||||||
|
|
||||||
class _PigeonCodec extends StandardMessageCodec {
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
const _PigeonCodec();
|
const _PigeonCodec();
|
||||||
@override
|
@override
|
||||||
@@ -35,9 +33,6 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
if (value is int) {
|
if (value is int) {
|
||||||
buffer.putUint8(4);
|
buffer.putUint8(4);
|
||||||
buffer.putInt64(value);
|
buffer.putInt64(value);
|
||||||
} else if (value is PermissionStatus) {
|
|
||||||
buffer.putUint8(129);
|
|
||||||
writeValue(buffer, value.index);
|
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(buffer, value);
|
super.writeValue(buffer, value);
|
||||||
}
|
}
|
||||||
@@ -46,9 +41,6 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
@override
|
@override
|
||||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 129:
|
|
||||||
final value = readValue(buffer) as int?;
|
|
||||||
return value == null ? null : PermissionStatus.values[value];
|
|
||||||
default:
|
default:
|
||||||
return super.readValueOfType(type, buffer);
|
return super.readValueOfType(type, buffer);
|
||||||
}
|
}
|
||||||
@@ -68,25 +60,6 @@ class PermissionApi {
|
|||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<PermissionStatus> isIgnoringBatteryOptimizations() async {
|
|
||||||
final pigeonVar_channelName =
|
|
||||||
'dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
|
||||||
);
|
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
|
||||||
pigeonVar_replyList,
|
|
||||||
pigeonVar_channelName,
|
|
||||||
isNullValid: false,
|
|
||||||
);
|
|
||||||
return pigeonVar_replyValue! as PermissionStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> hasManageMediaPermission() async {
|
Future<bool> hasManageMediaPermission() async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$pigeonVar_messageChannelSuffix';
|
||||||
|
|||||||
-191
@@ -1,191 +0,0 @@
|
|||||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
|
||||||
// See also: https://pub.dev/packages/pigeon
|
|
||||||
// ignore_for_file: unused_import, unused_shown_name
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
|
||||||
|
|
||||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
|
||||||
if (replyList == null) {
|
|
||||||
throw PlatformException(
|
|
||||||
code: 'channel-error',
|
|
||||||
message: 'Unable to establish connection on channel: "$channelName".',
|
|
||||||
);
|
|
||||||
} else if (replyList.length > 1) {
|
|
||||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
|
||||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
|
||||||
throw PlatformException(
|
|
||||||
code: 'null-error',
|
|
||||||
message: 'Host platform returned null value for non-null return value.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return replyList.firstOrNull;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _deepEquals(Object? a, Object? b) {
|
|
||||||
if (identical(a, b)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (a is double && b is double) {
|
|
||||||
if (a.isNaN && b.isNaN) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return a == b;
|
|
||||||
}
|
|
||||||
if (a is List && b is List) {
|
|
||||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
|
||||||
}
|
|
||||||
if (a is Map && b is Map) {
|
|
||||||
if (a.length != b.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (final MapEntry<Object?, Object?> entryA in a.entries) {
|
|
||||||
bool found = false;
|
|
||||||
for (final MapEntry<Object?, Object?> entryB in b.entries) {
|
|
||||||
if (_deepEquals(entryA.key, entryB.key)) {
|
|
||||||
if (_deepEquals(entryA.value, entryB.value)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return a == b;
|
|
||||||
}
|
|
||||||
|
|
||||||
int _deepHash(Object? value) {
|
|
||||||
if (value is List) {
|
|
||||||
return Object.hashAll(value.map(_deepHash));
|
|
||||||
}
|
|
||||||
if (value is Map) {
|
|
||||||
int result = 0;
|
|
||||||
for (final MapEntry<Object?, Object?> entry in value.entries) {
|
|
||||||
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (value is double && value.isNaN) {
|
|
||||||
// Normalize NaN to a consistent hash.
|
|
||||||
return 0x7FF8000000000000.hashCode;
|
|
||||||
}
|
|
||||||
if (value is double && value == 0.0) {
|
|
||||||
// Normalize -0.0 to 0.0 so they have the same hash code.
|
|
||||||
return 0.0.hashCode;
|
|
||||||
}
|
|
||||||
return value.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewIntentPayload {
|
|
||||||
ViewIntentPayload({this.path, required this.mimeType, this.localAssetId});
|
|
||||||
|
|
||||||
String? path;
|
|
||||||
|
|
||||||
String mimeType;
|
|
||||||
|
|
||||||
String? localAssetId;
|
|
||||||
|
|
||||||
List<Object?> _toList() {
|
|
||||||
return <Object?>[path, mimeType, localAssetId];
|
|
||||||
}
|
|
||||||
|
|
||||||
Object encode() {
|
|
||||||
return _toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
static ViewIntentPayload decode(Object result) {
|
|
||||||
result as List<Object?>;
|
|
||||||
return ViewIntentPayload(
|
|
||||||
path: result[0] as String?,
|
|
||||||
mimeType: result[1]! as String,
|
|
||||||
localAssetId: result[2] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other is! ViewIntentPayload || other.runtimeType != runtimeType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (identical(this, other)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return _deepEquals(path, other.path) &&
|
|
||||||
_deepEquals(mimeType, other.mimeType) &&
|
|
||||||
_deepEquals(localAssetId, other.localAssetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
|
||||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PigeonCodec extends StandardMessageCodec {
|
|
||||||
const _PigeonCodec();
|
|
||||||
@override
|
|
||||||
void writeValue(WriteBuffer buffer, Object? value) {
|
|
||||||
if (value is int) {
|
|
||||||
buffer.putUint8(4);
|
|
||||||
buffer.putInt64(value);
|
|
||||||
} else if (value is ViewIntentPayload) {
|
|
||||||
buffer.putUint8(129);
|
|
||||||
writeValue(buffer, value.encode());
|
|
||||||
} else {
|
|
||||||
super.writeValue(buffer, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
|
||||||
switch (type) {
|
|
||||||
case 129:
|
|
||||||
return ViewIntentPayload.decode(readValue(buffer)!);
|
|
||||||
default:
|
|
||||||
return super.readValueOfType(type, buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewIntentHostApi {
|
|
||||||
/// Constructor for [ViewIntentHostApi]. The [binaryMessenger] named argument is
|
|
||||||
/// available for dependency injection. If it is left null, the default
|
|
||||||
/// BinaryMessenger will be used which routes to the host platform.
|
|
||||||
ViewIntentHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
|
||||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
|
||||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
|
||||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
|
||||||
|
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
|
||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
|
||||||
|
|
||||||
Future<ViewIntentPayload?> consumeViewIntent() async {
|
|
||||||
final pigeonVar_channelName =
|
|
||||||
'dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$pigeonVar_messageChannelSuffix';
|
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
|
||||||
pigeonChannelCodec,
|
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
|
||||||
);
|
|
||||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
|
||||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
|
||||||
|
|
||||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
|
||||||
pigeonVar_replyList,
|
|
||||||
pigeonVar_channelName,
|
|
||||||
isNullValid: true,
|
|
||||||
);
|
|
||||||
return pigeonVar_replyValue as ViewIntentPayload?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,6 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.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/remote_album_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/remote_album_sliver_app_bar.dart';
|
||||||
import 'package:openapi/api.dart' show Optional;
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class RemoteAlbumPage extends ConsumerStatefulWidget {
|
class RemoteAlbumPage extends ConsumerStatefulWidget {
|
||||||
@@ -248,13 +247,10 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
|
|||||||
try {
|
try {
|
||||||
final newTitle = titleController.text.trim();
|
final newTitle = titleController.text.trim();
|
||||||
final newDescription = descriptionController.text.trim();
|
final newDescription = descriptionController.text.trim();
|
||||||
final description = newDescription.isEmpty
|
|
||||||
? const Optional<String?>.present(null)
|
|
||||||
: Optional<String?>.present(newDescription);
|
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(remoteAlbumProvider.notifier)
|
.read(remoteAlbumProvider.notifier)
|
||||||
.updateAlbum(widget.album.id, name: newTitle, description: description);
|
.updateAlbum(widget.album.id, name: newTitle, description: newDescription);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -11,9 +10,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
|
|||||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.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/providers/view_intent/view_intent_file_path.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
|
||||||
import 'package:immich_mobile/services/view_intent.service.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
@@ -30,11 +26,7 @@ class UploadActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isTimeline = source == ActionSource.timeline;
|
final isTimeline = source == ActionSource.timeline;
|
||||||
final viewerIntentFilePath = source == ActionSource.viewer ? ref.read(viewIntentFilePathProvider) : null;
|
|
||||||
List<LocalAsset>? assets;
|
List<LocalAsset>? assets;
|
||||||
var isUploadDialogOpen = false;
|
|
||||||
var wasUploadCancelled = false;
|
|
||||||
Future<void>? uploadDialogFuture;
|
|
||||||
|
|
||||||
if (source == ActionSource.timeline) {
|
if (source == ActionSource.timeline) {
|
||||||
assets = ref.read(multiSelectProvider).selectedAssets.whereType<LocalAsset>().toList();
|
assets = ref.read(multiSelectProvider).selectedAssets.whereType<LocalAsset>().toList();
|
||||||
@@ -43,50 +35,22 @@ class UploadActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
} else {
|
} else {
|
||||||
isUploadDialogOpen = true;
|
unawaited(
|
||||||
uploadDialogFuture =
|
showDialog(
|
||||||
showDialog<void>(
|
context: context,
|
||||||
context: context,
|
barrierDismissible: false,
|
||||||
barrierDismissible: false,
|
builder: (dialogContext) => const _UploadProgressDialog(),
|
||||||
builder: (dialogContext) => _UploadProgressDialog(
|
),
|
||||||
onCancel: () {
|
);
|
||||||
wasUploadCancelled = true;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).whenComplete(() {
|
|
||||||
isUploadDialogOpen = false;
|
|
||||||
});
|
|
||||||
unawaited(uploadDialogFuture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = false;
|
final result = await ref.read(actionProvider.notifier).upload(source, assets: assets);
|
||||||
if (!isTimeline && viewerIntentFilePath != null) {
|
|
||||||
final viewIntentService = ref.read(viewIntentServiceProvider);
|
|
||||||
viewIntentService.markUploadActive(viewerIntentFilePath);
|
|
||||||
var hasError = false;
|
|
||||||
try {
|
|
||||||
await ref
|
|
||||||
.read(foregroundUploadServiceProvider)
|
|
||||||
.uploadShareIntent(
|
|
||||||
[File(viewerIntentFilePath)],
|
|
||||||
onError: (_, _) {
|
|
||||||
hasError = true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await viewIntentService.markUploadInactive(viewerIntentFilePath);
|
|
||||||
}
|
|
||||||
success = !hasError;
|
|
||||||
} else {
|
|
||||||
final result = await ref.read(actionProvider.notifier).upload(source, assets: assets);
|
|
||||||
success = result.success;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTimeline && context.mounted && isUploadDialogOpen) {
|
if (!isTimeline && context.mounted) {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted && !success && !wasUploadCancelled) {
|
if (context.mounted && !result.success) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'scaffold_body_error_occurred'.t(context: context),
|
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
@@ -109,9 +73,7 @@ class UploadActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UploadProgressDialog extends ConsumerWidget {
|
class _UploadProgressDialog extends ConsumerWidget {
|
||||||
final VoidCallback onCancel;
|
const _UploadProgressDialog();
|
||||||
|
|
||||||
const _UploadProgressDialog({required this.onCancel});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -141,8 +103,7 @@ class _UploadProgressDialog extends ConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(manualUploadCancelTokenProvider)?.complete();
|
ref.read(manualUploadCancelTokenProvider)?.complete();
|
||||||
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
|
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
|
||||||
onCancel();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
|
||||||
},
|
},
|
||||||
labelText: 'cancel'.t(context: context),
|
labelText: 'cancel'.t(context: context),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.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/infrastructure/settings.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
|
|
||||||
@@ -324,16 +323,14 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
required PhotoViewHeroAttributes? heroAttributes,
|
required PhotoViewHeroAttributes? heroAttributes,
|
||||||
required bool isCurrent,
|
required bool isCurrent,
|
||||||
required bool isPlayingMotionVideo,
|
required bool isPlayingMotionVideo,
|
||||||
required String? localFilePath,
|
|
||||||
}) {
|
}) {
|
||||||
final size = context.sizeData;
|
final size = context.sizeData;
|
||||||
final imageProvider = getFullImageProvider(asset, size: size, localFilePath: localFilePath);
|
|
||||||
|
|
||||||
if (asset.isImage && !isPlayingMotionVideo) {
|
if (asset.isImage && !isPlayingMotionVideo) {
|
||||||
return PhotoView(
|
return PhotoView(
|
||||||
key: Key(asset.heroTag),
|
key: Key(asset.heroTag),
|
||||||
index: widget.index,
|
index: widget.index,
|
||||||
imageProvider: imageProvider,
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
heroAttributes: heroAttributes,
|
heroAttributes: heroAttributes,
|
||||||
loadingBuilder: (context, progress, index) => const Center(child: ImmichLoadingIndicator()),
|
loadingBuilder: (context, progress, index) => const Center(child: ImmichLoadingIndicator()),
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
@@ -380,9 +377,12 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
child: NativeVideoViewer(
|
child: NativeVideoViewer(
|
||||||
key: _NativeVideoViewerKey(asset.heroTag),
|
key: _NativeVideoViewerKey(asset.heroTag),
|
||||||
asset: asset,
|
asset: asset,
|
||||||
localFilePath: localFilePath,
|
|
||||||
isCurrent: isCurrent,
|
isCurrent: isCurrent,
|
||||||
image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center),
|
image: Image(
|
||||||
|
image: getFullImageProvider(asset, size: size),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -393,7 +393,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||||
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||||
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
||||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
|
||||||
|
|
||||||
final asset = _asset;
|
final asset = _asset;
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
@@ -422,8 +421,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
_scrollController.snapPosition.snapOffset = _snapOffset;
|
_scrollController.snapPosition.snapOffset = _snapOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
final viewIntentFilePath = timelineOrigin == TimelineOrigin.deepLink ? ref.watch(viewIntentFilePathProvider) : null;
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
@@ -443,7 +440,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
|||||||
: null,
|
: null,
|
||||||
isCurrent: isCurrent,
|
isCurrent: isCurrent,
|
||||||
isPlayingMotionVideo: isPlayingMotionVideo,
|
isPlayingMotionVideo: isPlayingMotionVideo,
|
||||||
localFilePath: viewIntentFilePath,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -20,7 +19,6 @@ import 'package:native_video_player/native_video_player.dart';
|
|||||||
|
|
||||||
class NativeVideoViewer extends ConsumerStatefulWidget {
|
class NativeVideoViewer extends ConsumerStatefulWidget {
|
||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
final String? localFilePath;
|
|
||||||
final bool isCurrent;
|
final bool isCurrent;
|
||||||
final bool showControls;
|
final bool showControls;
|
||||||
final Widget image;
|
final Widget image;
|
||||||
@@ -28,7 +26,6 @@ class NativeVideoViewer extends ConsumerStatefulWidget {
|
|||||||
const NativeVideoViewer({
|
const NativeVideoViewer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
this.localFilePath,
|
|
||||||
required this.image,
|
required this.image,
|
||||||
this.isCurrent = false,
|
this.isCurrent = false,
|
||||||
this.showControls = true,
|
this.showControls = true,
|
||||||
@@ -109,19 +106,6 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final localFilePath = widget.localFilePath;
|
|
||||||
if (localFilePath != null) {
|
|
||||||
final file = File(localFilePath);
|
|
||||||
if (!await file.exists()) {
|
|
||||||
throw Exception('No file found for the video');
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoSource.init(
|
|
||||||
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
|
|
||||||
type: VideoSourceType.file,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
||||||
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
||||||
final file = await StorageRepository().getFileForAsset(id);
|
final file = await StorageRepository().getFileForAsset(id);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
@@ -147,17 +146,10 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider getFullImageProvider(
|
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool edited = true}) {
|
||||||
BaseAsset asset, {
|
|
||||||
Size size = const Size(1080, 1920),
|
|
||||||
bool edited = true,
|
|
||||||
String? localFilePath,
|
|
||||||
}) {
|
|
||||||
// Create new provider and cache it
|
// Create new provider and cache it
|
||||||
final ImageProvider provider;
|
final ImageProvider provider;
|
||||||
if (localFilePath != null) {
|
if (_shouldUseLocalAsset(asset)) {
|
||||||
provider = FileImage(File(localFilePath));
|
|
||||||
} else if (_shouldUseLocalAsset(asset)) {
|
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type, isAnimated: asset.isAnimatedImage);
|
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type, isAnimated: asset.isAnimatedImage);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/time_range.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
||||||
@@ -16,7 +15,6 @@ class MapState {
|
|||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
final int relativeDays;
|
final int relativeDays;
|
||||||
final TimeRange timeRange;
|
|
||||||
|
|
||||||
const MapState({
|
const MapState({
|
||||||
this.themeMode = ThemeMode.system,
|
this.themeMode = ThemeMode.system,
|
||||||
@@ -25,7 +23,6 @@ class MapState {
|
|||||||
this.includeArchived = false,
|
this.includeArchived = false,
|
||||||
this.withPartners = false,
|
this.withPartners = false,
|
||||||
this.relativeDays = 0,
|
this.relativeDays = 0,
|
||||||
this.timeRange = const TimeRange(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -43,7 +40,6 @@ class MapState {
|
|||||||
bool? includeArchived,
|
bool? includeArchived,
|
||||||
bool? withPartners,
|
bool? withPartners,
|
||||||
int? relativeDays,
|
int? relativeDays,
|
||||||
TimeRange? timeRange,
|
|
||||||
}) {
|
}) {
|
||||||
return MapState(
|
return MapState(
|
||||||
bounds: bounds ?? this.bounds,
|
bounds: bounds ?? this.bounds,
|
||||||
@@ -52,7 +48,6 @@ class MapState {
|
|||||||
includeArchived: includeArchived ?? this.includeArchived,
|
includeArchived: includeArchived ?? this.includeArchived,
|
||||||
withPartners: withPartners ?? this.withPartners,
|
withPartners: withPartners ?? this.withPartners,
|
||||||
relativeDays: relativeDays ?? this.relativeDays,
|
relativeDays: relativeDays ?? this.relativeDays,
|
||||||
timeRange: timeRange ?? this.timeRange,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +57,6 @@ class MapState {
|
|||||||
includeArchived: includeArchived,
|
includeArchived: includeArchived,
|
||||||
withPartners: withPartners,
|
withPartners: withPartners,
|
||||||
relativeDays: relativeDays,
|
relativeDays: relativeDays,
|
||||||
timeRange: timeRange,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +103,6 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTimeRange(TimeRange range) {
|
|
||||||
ref.read(settingsProvider).write(.mapCustomFrom, range.from);
|
|
||||||
ref.read(settingsProvider).write(.mapCustomTo, range.to);
|
|
||||||
state = state.copyWith(timeRange: range);
|
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MapState build() {
|
MapState build() {
|
||||||
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
||||||
@@ -124,9 +111,8 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
onlyFavorites: mapConfig.favoritesOnly,
|
onlyFavorites: mapConfig.favoritesOnly,
|
||||||
includeArchived: mapConfig.includeArchived,
|
includeArchived: mapConfig.includeArchived,
|
||||||
withPartners: mapConfig.withPartners,
|
withPartners: mapConfig.withPartners,
|
||||||
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
|
||||||
relativeDays: mapConfig.relativeDays,
|
relativeDays: mapConfig.relativeDays,
|
||||||
timeRange: TimeRange(from: mapConfig.customFrom, to: mapConfig.customTo),
|
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/time_range.model.dart';
|
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_settings/map_custom_time_range.dart';
|
|
||||||
import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart';
|
import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart';
|
import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart';
|
import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart';
|
||||||
|
|
||||||
class DriftMapSettingsSheet extends ConsumerStatefulWidget {
|
class DriftMapSettingsSheet extends HookConsumerWidget {
|
||||||
const DriftMapSettingsSheet({super.key});
|
const DriftMapSettingsSheet({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<DriftMapSettingsSheet> createState() => _DriftMapSettingsSheetState();
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
}
|
|
||||||
|
|
||||||
class _DriftMapSettingsSheetState extends ConsumerState<DriftMapSettingsSheet> {
|
|
||||||
late bool useCustomRange;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final mapState = ref.read(mapStateProvider);
|
|
||||||
final timeRange = mapState.timeRange;
|
|
||||||
useCustomRange = timeRange.from.isSome || timeRange.to.isSome;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final mapState = ref.watch(mapStateProvider);
|
final mapState = ref.watch(mapStateProvider);
|
||||||
|
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
expand: false,
|
expand: false,
|
||||||
initialChildSize: useCustomRange ? 0.7 : 0.6,
|
initialChildSize: 0.6,
|
||||||
builder: (ctx, scrollController) => SingleChildScrollView(
|
builder: (ctx, scrollController) => SingleChildScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -65,41 +47,10 @@ class _DriftMapSettingsSheetState extends ConsumerState<DriftMapSettingsSheet> {
|
|||||||
selected: mapState.withPartners,
|
selected: mapState.withPartners,
|
||||||
onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners),
|
onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners),
|
||||||
),
|
),
|
||||||
if (useCustomRange) ...[
|
MapTimeDropDown(
|
||||||
MapTimeRange(
|
relativeTime: mapState.relativeDays,
|
||||||
timeRange: mapState.timeRange,
|
onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time),
|
||||||
onChanged: (range) {
|
),
|
||||||
ref.read(mapStateProvider.notifier).setTimeRange(range);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => setState(() {
|
|
||||||
useCustomRange = false;
|
|
||||||
ref.read(mapStateProvider.notifier).setRelativeTime(0);
|
|
||||||
ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange());
|
|
||||||
}),
|
|
||||||
child: Text(context.t.remove_custom_date_range),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
MapTimeDropDown(
|
|
||||||
relativeTime: mapState.relativeDays,
|
|
||||||
onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => setState(() {
|
|
||||||
useCustomRange = true;
|
|
||||||
ref.read(mapStateProvider.notifier).setRelativeTime(0);
|
|
||||||
ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange());
|
|
||||||
}),
|
|
||||||
child: Text(context.t.use_custom_date_range),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/permission.provider.dart';
|
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import 'dart:io';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
import 'package:immich_mobile/models/upload/share_intent_attachment.model.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/share_intent_service.dart';
|
import 'package:immich_mobile/services/share_intent_service.dart';
|
||||||
|
import 'package:immich_mobile/services/foreground_upload.service.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;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
|||||||
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
|
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
|
||||||
_updateProgress(fileId, progress);
|
_updateProgress(fileId, progress);
|
||||||
},
|
},
|
||||||
onSuccess: (fileId, _) {
|
onSuccess: (fileId) {
|
||||||
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
|
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
|
||||||
},
|
},
|
||||||
onError: (fileId, errorMessage) {
|
onError: (fileId, errorMessage) {
|
||||||
|
|||||||
@@ -36,12 +36,11 @@ class ActionResult {
|
|||||||
final int count;
|
final int count;
|
||||||
final bool success;
|
final bool success;
|
||||||
final String? error;
|
final String? error;
|
||||||
final List<String> remoteAssetIds;
|
|
||||||
|
|
||||||
const ActionResult({required this.count, required this.success, this.error, this.remoteAssetIds = const []});
|
const ActionResult({required this.count, required this.success, this.error});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ActionResult(count: $count, success: $success, error: $error, remoteAssetIds: $remoteAssetIds)';
|
String toString() => 'ActionResult(count: $count, success: $success, error: $error)';
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionNotifier extends Notifier<void> {
|
class ActionNotifier extends Notifier<void> {
|
||||||
@@ -555,14 +554,10 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
final uploadedAssetIds = <String>{};
|
final uploadedAssetIds = <String>{};
|
||||||
final failedAssetIds = <String>{};
|
final failedAssetIds = <String>{};
|
||||||
final postUploadTasks = <Future<void>>[];
|
final postUploadTasks = <Future<void>>[];
|
||||||
if (assetsToUpload.isEmpty) {
|
|
||||||
return const ActionResult(count: 0, success: false, error: 'No assets to upload');
|
|
||||||
}
|
|
||||||
|
|
||||||
final progressNotifier = ref.read(assetUploadProgressProvider.notifier);
|
final progressNotifier = ref.read(assetUploadProgressProvider.notifier);
|
||||||
final cancelToken = Completer<void>();
|
final cancelToken = Completer<void>();
|
||||||
ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken;
|
ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken;
|
||||||
final remoteAssetIds = <String>[];
|
|
||||||
|
|
||||||
// Initialize progress for all assets
|
// Initialize progress for all assets
|
||||||
for (final asset in assetsToUpload) {
|
for (final asset in assetsToUpload) {
|
||||||
@@ -579,7 +574,6 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
progressNotifier.setProgress(localAssetId, progress);
|
progressNotifier.setProgress(localAssetId, progress);
|
||||||
},
|
},
|
||||||
onSuccess: (localAssetId, remoteAssetId) {
|
onSuccess: (localAssetId, remoteAssetId) {
|
||||||
remoteAssetIds.add(remoteAssetId);
|
|
||||||
progressNotifier.remove(localAssetId);
|
progressNotifier.remove(localAssetId);
|
||||||
uploadedAssetIds.add(localAssetId);
|
uploadedAssetIds.add(localAssetId);
|
||||||
final asset = assetById[localAssetId];
|
final asset = assetById[localAssetId];
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
/// Holds the isolate's cancellation signal.
|
/// Provider holding a boolean function that returns true when cancellation is requested.
|
||||||
final cancellationProvider = Provider<Completer<void>>(
|
/// A computation running in the isolate uses the function to implement cooperative cancellation.
|
||||||
|
final cancellationProvider = Provider<bool Function()>(
|
||||||
// This will be overridden in the isolate's container.
|
// This will be overridden in the isolate's container.
|
||||||
// Throwing ensures it's not used without an override.
|
// Throwing ensures it's not used without an override.
|
||||||
(ref) => throw UnimplementedError(
|
(ref) => throw UnimplementedError(
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
import 'package:openapi/api.dart' show Optional;
|
|
||||||
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
|
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
@@ -154,7 +153,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
Future<RemoteAlbum?> updateAlbum(
|
Future<RemoteAlbum?> updateAlbum(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
String? name,
|
String? name,
|
||||||
Optional<String?> description = const Optional.absent(),
|
String? description,
|
||||||
String? thumbnailAssetId,
|
String? thumbnailAssetId,
|
||||||
bool? isActivityEnabled,
|
bool? isActivityEnabled,
|
||||||
AlbumAssetOrder? order,
|
AlbumAssetOrder? order,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final syncStreamServiceProvider = Provider(
|
|||||||
permissionRepository: ref.watch(permissionRepositoryProvider),
|
permissionRepository: ref.watch(permissionRepositoryProvider),
|
||||||
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
|
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
|
||||||
api: ref.watch(apiServiceProvider),
|
api: ref.watch(apiServiceProvider),
|
||||||
cancellation: ref.watch(cancellationProvider),
|
cancelChecker: ref.watch(cancellationProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,7 +42,6 @@ final localSyncServiceProvider = Provider(
|
|||||||
assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
|
assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
|
||||||
permissionRepository: ref.watch(permissionRepositoryProvider),
|
permissionRepository: ref.watch(permissionRepositoryProvider),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||||
cancellation: ref.watch(cancellationProvider),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -52,6 +51,5 @@ final hashServiceProvider = Provider(
|
|||||||
localAssetRepository: ref.watch(localAssetRepository),
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||||
cancellation: ref.watch(cancellationProvider),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
-26
@@ -1,9 +1,6 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/platform/permission_api.g.dart' as pm;
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class NotificationPermissionNotifier extends StateNotifier<PermissionStatus> {
|
class NotificationPermissionNotifier extends StateNotifier<PermissionStatus> {
|
||||||
@@ -42,26 +39,3 @@ class NotificationPermissionNotifier extends StateNotifier<PermissionStatus> {
|
|||||||
final notificationPermissionProvider = StateNotifierProvider<NotificationPermissionNotifier, PermissionStatus>(
|
final notificationPermissionProvider = StateNotifierProvider<NotificationPermissionNotifier, PermissionStatus>(
|
||||||
(ref) => NotificationPermissionNotifier(),
|
(ref) => NotificationPermissionNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final batteryOptimizationProvider = AsyncNotifierProvider<BatteryOptimizationNotifier, PermissionStatus>(
|
|
||||||
BatteryOptimizationNotifier.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
class BatteryOptimizationNotifier extends AsyncNotifier<PermissionStatus> {
|
|
||||||
Future<PermissionStatus> getBatteryOptimizationPermission() async {
|
|
||||||
final isIgnoring = await ref.read(permissionApiProvider).isIgnoringBatteryOptimizations().then((p) => p.toStatus());
|
|
||||||
state = AsyncValue.data(isIgnoring);
|
|
||||||
return isIgnoring;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<PermissionStatus> build() => getBatteryOptimizationPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on pm.PermissionStatus {
|
|
||||||
PermissionStatus toStatus() => switch (this) {
|
|
||||||
pm.PermissionStatus.granted => PermissionStatus.granted,
|
|
||||||
pm.PermissionStatus.denied => PermissionStatus.denied,
|
|
||||||
pm.PermissionStatus.permanentlyDenied => PermissionStatus.permanentlyDenied,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ final getAllPlacesProvider = FutureProvider.autoDispose<List<SearchCuratedConten
|
|||||||
}
|
}
|
||||||
|
|
||||||
final curatedContent = assetPlaces
|
final curatedContent = assetPlaces
|
||||||
.map((data) => SearchCuratedContent(label: data.exifInfo.orElse(null)!.city.orElse(null)!, id: data.id))
|
.map((data) => SearchCuratedContent(label: data.exifInfo!.city!, id: data.id))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return curatedContent;
|
return curatedContent;
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
class ViewIntentFilePathNotifier extends Notifier<String?> {
|
|
||||||
@override
|
|
||||||
String? build() => null;
|
|
||||||
|
|
||||||
void setPath(String path) {
|
|
||||||
if (state == path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
|
||||||
if (state == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearIfMatch(String path) {
|
|
||||||
if (state != path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final viewIntentFilePathProvider = NotifierProvider<ViewIntentFilePathNotifier, String?>(
|
|
||||||
ViewIntentFilePathNotifier.new,
|
|
||||||
);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler_android.dart';
|
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler_stub.dart';
|
|
||||||
|
|
||||||
abstract class ViewIntentHandler {
|
|
||||||
void init();
|
|
||||||
|
|
||||||
Future<void> onAppResumed();
|
|
||||||
|
|
||||||
Future<void> flushDeferredViewIntent();
|
|
||||||
|
|
||||||
Future<void> handle(ViewIntentPayload attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
final viewIntentHandlerProvider = Provider<ViewIntentHandler>((ref) {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
return AndroidViewIntentHandler(ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const StubViewIntentHandler();
|
|
||||||
});
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
|
||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_pending.provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/services/view_intent.service.dart';
|
|
||||||
import 'package:immich_mobile/services/view_intent_asset_resolver.service.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
class AndroidViewIntentHandler implements ViewIntentHandler {
|
|
||||||
final Ref _ref;
|
|
||||||
final ViewIntentService _viewIntentService;
|
|
||||||
final ViewIntentAssetResolver _viewIntentAssetResolver;
|
|
||||||
final AppRouter _router;
|
|
||||||
static final Logger _logger = Logger('ViewIntentHandler');
|
|
||||||
|
|
||||||
AndroidViewIntentHandler(Ref ref)
|
|
||||||
: _ref = ref,
|
|
||||||
_viewIntentService = ref.read(viewIntentServiceProvider),
|
|
||||||
_viewIntentAssetResolver = ref.read(viewIntentAssetResolverProvider),
|
|
||||||
_router = ref.watch(appRouterProvider);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void init() {
|
|
||||||
// Covers cold start from a view intent before the first lifecycle "resumed".
|
|
||||||
unawaited(onAppResumed());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAppResumed() => _checkForViewIntent();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> flushDeferredViewIntent() => _flushPending();
|
|
||||||
|
|
||||||
Future<void> _checkForViewIntent() async {
|
|
||||||
final attachment = await _viewIntentService.consumeViewIntent();
|
|
||||||
if (attachment != null) {
|
|
||||||
await handle(attachment);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_ref.read(viewIntentPendingProvider) == null) {
|
|
||||||
await _viewIntentService.cleanupStaleTempFiles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _flushPending() async {
|
|
||||||
final pendingAttachment = _ref.read(viewIntentPendingProvider.notifier).takeIfFresh();
|
|
||||||
_logger.info('flushPending, pendingAttachment:$pendingAttachment');
|
|
||||||
if (pendingAttachment != null) {
|
|
||||||
await handle(pendingAttachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> handle(ViewIntentPayload attachment) async {
|
|
||||||
_logger.info(
|
|
||||||
'handle attachment, mimeType:${attachment.mimeType}, localAssetId=${attachment.localAssetId}, path=${attachment.path}, isAuthenticated:${_ref.read(authProvider).isAuthenticated}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!_ref.read(authProvider).isAuthenticated) {
|
|
||||||
_ref.read(viewIntentPendingProvider.notifier).defer(attachment);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final resolvedAsset = await _viewIntentAssetResolver.resolve(attachment);
|
|
||||||
_logger.fine('resolved view intent asset: ${resolvedAsset.asset}');
|
|
||||||
await _openAssetViewer(
|
|
||||||
resolvedAsset.asset,
|
|
||||||
resolvedAsset.timelineService,
|
|
||||||
viewIntentFilePath: resolvedAsset.viewIntentFilePath,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openAssetViewer(BaseAsset asset, TimelineService timelineService, {String? viewIntentFilePath}) async {
|
|
||||||
final notifier = _ref.read(assetViewerProvider.notifier);
|
|
||||||
notifier.reset();
|
|
||||||
if (asset.isVideo) {
|
|
||||||
notifier.setControls(false);
|
|
||||||
}
|
|
||||||
notifier.setAsset(asset);
|
|
||||||
|
|
||||||
if (viewIntentFilePath != null) {
|
|
||||||
_ref.read(viewIntentFilePathProvider.notifier).setPath(viewIntentFilePath);
|
|
||||||
unawaited(_viewIntentService.setManagedTempFilePath(viewIntentFilePath));
|
|
||||||
} else {
|
|
||||||
_ref.read(viewIntentFilePathProvider.notifier).clear();
|
|
||||||
unawaited(_viewIntentService.cleanupManagedTempFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
await _router.replaceAll([
|
|
||||||
const TabShellRoute(),
|
|
||||||
AssetViewerRoute(initialIndex: 0, timelineService: timelineService),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
|
||||||
|
|
||||||
class StubViewIntentHandler implements ViewIntentHandler {
|
|
||||||
const StubViewIntentHandler();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void init() {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAppResumed() async {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> flushDeferredViewIntent() async {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> handle(ViewIntentPayload attachment) async {}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
|
|
||||||
final viewIntentNowProvider = Provider<DateTime Function()>((ref) => DateTime.now);
|
|
||||||
|
|
||||||
final viewIntentPendingProvider = NotifierProvider<ViewIntentPendingNotifier, ViewIntentPayload?>(
|
|
||||||
ViewIntentPendingNotifier.new,
|
|
||||||
);
|
|
||||||
|
|
||||||
class ViewIntentPendingNotifier extends Notifier<ViewIntentPayload?> {
|
|
||||||
static const _ttl = Duration(minutes: 10);
|
|
||||||
|
|
||||||
DateTime? _deferredAt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewIntentPayload? build() => null;
|
|
||||||
|
|
||||||
void defer(ViewIntentPayload attachment) {
|
|
||||||
_deferredAt = ref.read(viewIntentNowProvider)();
|
|
||||||
state = attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewIntentPayload? takeIfFresh() {
|
|
||||||
final attachment = state;
|
|
||||||
final deferredAt = _deferredAt;
|
|
||||||
state = null;
|
|
||||||
_deferredAt = null;
|
|
||||||
|
|
||||||
if (attachment == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deferredAt != null && ref.read(viewIntentNowProvider)().difference(deferredAt) > _ttl) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,8 +23,8 @@ class ActivityApiRepository extends ApiRepository {
|
|||||||
final dto = ActivityCreateDto(
|
final dto = ActivityCreateDto(
|
||||||
albumId: albumId,
|
albumId: albumId,
|
||||||
type: type == ActivityType.comment ? ReactionType.comment : ReactionType.like,
|
type: type == ActivityType.comment ? ReactionType.comment : ReactionType.like,
|
||||||
assetId: assetId == null ? const Optional.absent() : Optional.present(assetId),
|
assetId: assetId,
|
||||||
comment: comment == null ? const Optional.absent() : Optional.present(comment),
|
comment: comment,
|
||||||
);
|
);
|
||||||
final response = await checkNull(_api.createActivity(dto));
|
final response = await checkNull(_api.createActivity(dto));
|
||||||
return _toActivity(response);
|
return _toActivity(response);
|
||||||
@@ -45,6 +45,6 @@ class ActivityApiRepository extends ApiRepository {
|
|||||||
type: dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like,
|
type: dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like,
|
||||||
user: UserConverter.fromSimpleUserDto(dto.user),
|
user: UserConverter.fromSimpleUserDto(dto.user),
|
||||||
assetId: dto.assetId,
|
assetId: dto.assetId,
|
||||||
comment: dto.comment.orElse(null),
|
comment: dto.comment,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
AssetApiRepository(this._api, this._stacksApi, this._trashApi);
|
AssetApiRepository(this._api, this._stacksApi, this._trashApi);
|
||||||
|
|
||||||
Future<void> delete(List<String> ids, bool force) async {
|
Future<void> delete(List<String> ids, bool force) async {
|
||||||
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: Optional.present(force)));
|
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreTrash(List<String> ids) async {
|
Future<void> restoreTrash(List<String> ids) async {
|
||||||
@@ -42,27 +42,19 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
|
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
|
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite)));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
||||||
return _api.updateAssets(
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
||||||
AssetBulkUpdateDto(
|
|
||||||
ids: ids,
|
|
||||||
latitude: Optional.present(location.latitude),
|
|
||||||
longitude: Optional.present(location.longitude),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
||||||
return _api.updateAssets(
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: dateTime.toIso8601String()));
|
||||||
AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime.toIso8601String())),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StackResponse> stack(List<String> ids) async {
|
Future<StackResponse> stack(List<String> ids) async {
|
||||||
@@ -90,15 +82,15 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
final response = await checkNull(_api.getAssetInfo(assetId));
|
final response = await checkNull(_api.getAssetInfo(assetId));
|
||||||
|
|
||||||
// we need to get the MIME of the thumbnail once that gets added to the API
|
// we need to get the MIME of the thumbnail once that gets added to the API
|
||||||
return response.originalMimeType.orElse(null);
|
return response.originalMimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDescription(String assetId, String description) {
|
Future<void> updateDescription(String assetId, String description) {
|
||||||
return _api.updateAsset(assetId, UpdateAssetDto(description: Optional.present(description)));
|
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateRating(String assetId, int rating) {
|
Future<void> updateRating(String assetId, int rating) {
|
||||||
return _api.updateAsset(assetId, UpdateAssetDto(rating: Optional.present(rating)));
|
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AssetEditsResponseDto?> editAsset(String assetId, List<AssetEdit> edits) {
|
Future<AssetEditsResponseDto?> editAsset(String assetId, List<AssetEdit> edits) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class AuthApiRepository extends ApiRepository {
|
|||||||
AuthApiRepository(this._apiService);
|
AuthApiRepository(this._apiService);
|
||||||
|
|
||||||
Future<void> changePassword(String newPassword) async {
|
Future<void> changePassword(String newPassword) async {
|
||||||
await _apiService.usersApi.updateMyUser(UserUpdateMeDto(password: Optional.present(newPassword)));
|
await _apiService.usersApi.updateMyUser(UserUpdateMeDto(password: newPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<LoginResponse> login(String email, String password) async {
|
Future<LoginResponse> login(String email, String password) async {
|
||||||
@@ -46,7 +46,7 @@ class AuthApiRepository extends ApiRepository {
|
|||||||
|
|
||||||
Future<bool> unlockPinCode(String pinCode) async {
|
Future<bool> unlockPinCode(String pinCode) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: Optional.present(pinCode)));
|
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: pinCode));
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -22,13 +22,7 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
String? description,
|
String? description,
|
||||||
}) async {
|
}) async {
|
||||||
final responseDto = await checkNull(
|
final responseDto = await checkNull(
|
||||||
_api.createAlbum(
|
_api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())),
|
||||||
CreateAlbumDto(
|
|
||||||
albumName: name,
|
|
||||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
|
||||||
assetIds: Optional.present(assetIds.toList()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return responseDto.toRemoteAlbum(owner);
|
return responseDto.toRemoteAlbum(owner);
|
||||||
@@ -47,14 +41,8 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
return (removed: removed, failed: failed);
|
return (removed: removed, failed: failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<({List<String> added, List<String> failed})> addAssets(
|
Future<({List<String> added, List<String> failed})> addAssets(String albumId, Iterable<String> assetIds) async {
|
||||||
String albumId,
|
final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList())));
|
||||||
Iterable<String> assetIds, {
|
|
||||||
Future<void>? abortTrigger,
|
|
||||||
}) async {
|
|
||||||
final response = await checkNull(
|
|
||||||
_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()), abortTrigger: abortTrigger),
|
|
||||||
);
|
|
||||||
final List<String> added = [], failed = [];
|
final List<String> added = [], failed = [];
|
||||||
for (final dto in response) {
|
for (final dto in response) {
|
||||||
if (dto.success) {
|
if (dto.success) {
|
||||||
@@ -71,7 +59,7 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
String albumId,
|
String albumId,
|
||||||
UserDto owner, {
|
UserDto owner, {
|
||||||
String? name,
|
String? name,
|
||||||
Optional<String?> description = const Optional.absent(),
|
String? description,
|
||||||
String? thumbnailAssetId,
|
String? thumbnailAssetId,
|
||||||
bool? isActivityEnabled,
|
bool? isActivityEnabled,
|
||||||
AlbumAssetOrder? order,
|
AlbumAssetOrder? order,
|
||||||
@@ -85,13 +73,11 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
_api.updateAlbumInfo(
|
_api.updateAlbumInfo(
|
||||||
albumId,
|
albumId,
|
||||||
UpdateAlbumDto(
|
UpdateAlbumDto(
|
||||||
albumName: name == null ? const Optional.absent() : Optional.present(name),
|
albumName: name,
|
||||||
description: description,
|
description: description,
|
||||||
albumThumbnailAssetId: thumbnailAssetId == null
|
albumThumbnailAssetId: thumbnailAssetId,
|
||||||
? const Optional.absent()
|
isActivityEnabled: isActivityEnabled,
|
||||||
: Optional.present(thumbnailAssetId),
|
order: apiOrder,
|
||||||
isActivityEnabled: isActivityEnabled == null ? const Optional.absent() : Optional.present(isActivityEnabled),
|
|
||||||
order: apiOrder == null ? const Optional.absent() : Optional.present(apiOrder),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -113,9 +99,7 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> setActivityStatus(String albumId, bool isEnabled) async {
|
Future<bool> setActivityStatus(String albumId, bool isEnabled) async {
|
||||||
final response = await checkNull(
|
final response = await checkNull(_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: isEnabled)));
|
||||||
_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: Optional.present(isEnabled))),
|
|
||||||
);
|
|
||||||
return response.isActivityEnabled;
|
return response.isActivityEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +116,7 @@ extension on AlbumResponseDto {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
thumbnailAssetId: albumThumbnailAssetId,
|
thumbnailAssetId: albumThumbnailAssetId,
|
||||||
isActivityEnabled: isActivityEnabled,
|
isActivityEnabled: isActivityEnabled,
|
||||||
order: order.orElse(null) == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
|
order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
|
||||||
assetCount: assetCount,
|
assetCount: assetCount,
|
||||||
isShared: albumUsers.length > 2,
|
isShared: albumUsers.length > 2,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PartnerApiRepository extends ApiRepository {
|
|||||||
|
|
||||||
Future<List<UserDto>> getAll(Direction direction) async {
|
Future<List<UserDto>> getAll(Direction direction) async {
|
||||||
final response = await checkNull(
|
final response = await checkNull(
|
||||||
_api.getPartners(direction == Direction.sharedByMe ? PartnerDirection.sharedBy : PartnerDirection.sharedWith),
|
_api.getPartners(direction == Direction.sharedByMe ? PartnerDirection.by : PartnerDirection.with_),
|
||||||
);
|
);
|
||||||
return response.map(UserConverter.fromPartnerDto).toList();
|
return response.map(UserConverter.fromPartnerDto).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ class PersonApiRepository extends ApiRepository {
|
|||||||
|
|
||||||
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
||||||
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
|
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
|
||||||
final dto = PersonUpdateDto(
|
final dto = PersonUpdateDto(name: name, birthDate: birthdayUtc);
|
||||||
name: name == null ? const Optional.absent() : Optional.present(name),
|
|
||||||
birthDate: birthdayUtc == null ? const Optional.absent() : Optional.present(birthdayUtc),
|
|
||||||
);
|
|
||||||
final response = await checkNull(_api.updatePerson(id, dto));
|
final response = await checkNull(_api.updatePerson(id, dto));
|
||||||
return _toPerson(response);
|
return _toPerson(response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ class SessionsAPIRepository extends ApiRepository {
|
|||||||
|
|
||||||
Future<SessionCreateResponse> createSession(String deviceType, String deviceOS, {int? duration}) async {
|
Future<SessionCreateResponse> createSession(String deviceType, String deviceOS, {int? duration}) async {
|
||||||
final dto = await checkNull(
|
final dto = await checkNull(
|
||||||
_api.createSession(
|
_api.createSession(SessionCreateDto(deviceType: deviceType, deviceOS: deviceOS, duration: duration)),
|
||||||
SessionCreateDto(
|
|
||||||
deviceType: Optional.present(deviceType),
|
|
||||||
deviceOS: Optional.present(deviceOS),
|
|
||||||
duration: duration == null ? const Optional.absent() : Optional.present(duration),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return SessionCreateResponse(
|
return SessionCreateResponse(
|
||||||
@@ -29,7 +23,7 @@ class SessionsAPIRepository extends ApiRepository {
|
|||||||
current: dto.current,
|
current: dto.current,
|
||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
deviceOS: deviceOS,
|
deviceOS: deviceOS,
|
||||||
expiresAt: dto.expiresAt.orElse(null),
|
expiresAt: dto.expiresAt,
|
||||||
createdAt: dto.createdAt,
|
createdAt: dto.createdAt,
|
||||||
updatedAt: dto.updatedAt,
|
updatedAt: dto.updatedAt,
|
||||||
token: dto.token,
|
token: dto.token,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class LockedGuard extends AutoRouteGuard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: Optional.present(securePinCode)));
|
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: securePinCode));
|
||||||
|
|
||||||
resolver.next(true);
|
resolver.next(true);
|
||||||
} on PlatformException catch (error) {
|
} on PlatformException catch (error) {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class ForegroundUploadService {
|
|||||||
List<File> files, {
|
List<File> files, {
|
||||||
Completer<void>? cancelToken,
|
Completer<void>? cancelToken,
|
||||||
void Function(String fileId, int bytes, int totalBytes)? onProgress,
|
void Function(String fileId, int bytes, int totalBytes)? onProgress,
|
||||||
void Function(String fileId, String remoteAssetId)? onSuccess,
|
void Function(String fileId)? onSuccess,
|
||||||
void Function(String fileId, String errorMessage)? onError,
|
void Function(String fileId, String errorMessage)? onError,
|
||||||
}) async {
|
}) async {
|
||||||
if (files.isEmpty) {
|
if (files.isEmpty) {
|
||||||
@@ -171,7 +171,7 @@ class ForegroundUploadService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
onSuccess?.call(fileId, result.remoteAssetId!);
|
onSuccess?.call(fileId);
|
||||||
} else if (!result.isCancelled && result.errorMessage != null) {
|
} else if (!result.isCancelled && result.errorMessage != null) {
|
||||||
onError?.call(fileId, result.errorMessage!);
|
onError?.call(fileId, result.errorMessage!);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ class OAuthService {
|
|||||||
log.info("Starting OAuth flow with redirect URI: $redirectUri");
|
log.info("Starting OAuth flow with redirect URI: $redirectUri");
|
||||||
|
|
||||||
final dto = await _apiService.oAuthApi.startOAuth(
|
final dto = await _apiService.oAuthApi.startOAuth(
|
||||||
OAuthConfigDto(
|
OAuthConfigDto(redirectUri: redirectUri, state: state, codeChallenge: codeChallenge),
|
||||||
redirectUri: redirectUri,
|
|
||||||
state: Optional.present(state),
|
|
||||||
codeChallenge: Optional.present(codeChallenge),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final authUrl = dto?.url;
|
final authUrl = dto?.url;
|
||||||
@@ -41,7 +37,7 @@ class OAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await _apiService.oAuthApi.finishOAuth(
|
return await _apiService.oAuthApi.finishOAuth(
|
||||||
OAuthCallbackDto(url: result, state: Optional.present(state), codeVerifier: Optional.present(codeVerifier)),
|
OAuthCallbackDto(url: result, state: state, codeVerifier: codeVerifier),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,26 +48,26 @@ class SharedLinkService {
|
|||||||
if (type == SharedLinkType.ALBUM) {
|
if (type == SharedLinkType.ALBUM) {
|
||||||
dto = SharedLinkCreateDto(
|
dto = SharedLinkCreateDto(
|
||||||
type: type,
|
type: type,
|
||||||
albumId: albumId == null ? const Optional.absent() : Optional.present(albumId),
|
albumId: albumId,
|
||||||
showMetadata: Optional.present(showMeta),
|
showMetadata: showMeta,
|
||||||
allowDownload: Optional.present(allowDownload),
|
allowDownload: allowDownload,
|
||||||
allowUpload: Optional.present(allowUpload),
|
allowUpload: allowUpload,
|
||||||
expiresAt: expiresAt == null ? const Optional.absent() : Optional.present(expiresAt),
|
expiresAt: expiresAt,
|
||||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
description: description,
|
||||||
password: password == null ? const Optional.absent() : Optional.present(password),
|
password: password,
|
||||||
slug: slug == null ? const Optional.absent() : Optional.present(slug),
|
slug: slug,
|
||||||
);
|
);
|
||||||
} else if (assetIds != null) {
|
} else if (assetIds != null) {
|
||||||
dto = SharedLinkCreateDto(
|
dto = SharedLinkCreateDto(
|
||||||
type: type,
|
type: type,
|
||||||
showMetadata: Optional.present(showMeta),
|
showMetadata: showMeta,
|
||||||
allowDownload: Optional.present(allowDownload),
|
allowDownload: allowDownload,
|
||||||
allowUpload: Optional.present(allowUpload),
|
allowUpload: allowUpload,
|
||||||
expiresAt: expiresAt == null ? const Optional.absent() : Optional.present(expiresAt),
|
expiresAt: expiresAt,
|
||||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
description: description,
|
||||||
password: password == null ? const Optional.absent() : Optional.present(password),
|
password: password,
|
||||||
slug: slug == null ? const Optional.absent() : Optional.present(slug),
|
slug: slug,
|
||||||
assetIds: Optional.present(assetIds),
|
assetIds: assetIds,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,22 +88,24 @@ class SharedLinkService {
|
|||||||
required bool? showMeta,
|
required bool? showMeta,
|
||||||
required bool? allowDownload,
|
required bool? allowDownload,
|
||||||
required bool? allowUpload,
|
required bool? allowUpload,
|
||||||
Optional<String?> password = const Optional.absent(),
|
bool? changeExpiry = false,
|
||||||
Optional<String?> description = const Optional.absent(),
|
String? description,
|
||||||
|
String? password,
|
||||||
String? slug,
|
String? slug,
|
||||||
Optional<DateTime?> expiresAt = const Optional.absent(),
|
DateTime? expiresAt,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final responseDto = await _apiService.sharedLinksApi.updateSharedLink(
|
final responseDto = await _apiService.sharedLinksApi.updateSharedLink(
|
||||||
id,
|
id,
|
||||||
SharedLinkEditDto(
|
SharedLinkEditDto(
|
||||||
showMetadata: showMeta == null ? const Optional.absent() : Optional.present(showMeta),
|
showMetadata: showMeta,
|
||||||
allowDownload: allowDownload == null ? const Optional.absent() : Optional.present(allowDownload),
|
allowDownload: allowDownload,
|
||||||
allowUpload: allowUpload == null ? const Optional.absent() : Optional.present(allowUpload),
|
allowUpload: allowUpload,
|
||||||
password: password,
|
|
||||||
description: description,
|
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
slug: slug == null ? const Optional.absent() : Optional.present(slug),
|
description: description,
|
||||||
|
password: password,
|
||||||
|
slug: slug,
|
||||||
|
changeExpiryTime: changeExpiry,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (responseDto != null) {
|
if (responseDto != null) {
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
final viewIntentServiceProvider = Provider((ref) => ViewIntentService(ViewIntentHostApi()));
|
|
||||||
|
|
||||||
class ViewIntentService {
|
|
||||||
final ViewIntentHostApi _viewIntentHostApi;
|
|
||||||
final Future<Directory> Function() _temporaryDirectory;
|
|
||||||
String? _managedTempFilePath;
|
|
||||||
final Set<String> _activeUploadPaths = {};
|
|
||||||
|
|
||||||
ViewIntentService(this._viewIntentHostApi, {Future<Directory> Function()? temporaryDirectory})
|
|
||||||
: _temporaryDirectory = temporaryDirectory ?? getTemporaryDirectory;
|
|
||||||
|
|
||||||
Future<ViewIntentPayload?> consumeViewIntent() async {
|
|
||||||
try {
|
|
||||||
return await _viewIntentHostApi.consumeViewIntent();
|
|
||||||
} catch (_) {
|
|
||||||
// Ignore errors - view intent might not be present
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setManagedTempFilePath(String path) async {
|
|
||||||
final previous = _managedTempFilePath;
|
|
||||||
if (previous == path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_managedTempFilePath = path;
|
|
||||||
if (previous != null) {
|
|
||||||
await cleanupTempFile(previous);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cleanupManagedTempFile() async {
|
|
||||||
final path = _managedTempFilePath;
|
|
||||||
_managedTempFilePath = null;
|
|
||||||
if (path != null) {
|
|
||||||
await cleanupTempFile(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cleanupManagedTempFileIfCurrent(String path) async {
|
|
||||||
if (_managedTempFilePath == path) {
|
|
||||||
_managedTempFilePath = null;
|
|
||||||
}
|
|
||||||
await cleanupTempFile(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cleanupTempFile(String path) async {
|
|
||||||
if (!_isManagedTempFile(path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_activeUploadPaths.contains(path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final file = File(path);
|
|
||||||
if (await file.exists()) {
|
|
||||||
await file.delete();
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// Best-effort cleanup only.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cleanupStaleTempFiles() async {
|
|
||||||
try {
|
|
||||||
final tempDirectory = await _temporaryDirectory();
|
|
||||||
await for (final entity in tempDirectory.list()) {
|
|
||||||
if (entity is! File) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final path = entity.path;
|
|
||||||
if (!_isManagedTempFile(path) || path == _managedTempFilePath || _activeUploadPaths.contains(path)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await entity.delete();
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// Best-effort cleanup only.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void markUploadActive(String path) {
|
|
||||||
_activeUploadPaths.add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> markUploadInactive(String path) async {
|
|
||||||
if (!_activeUploadPaths.remove(path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_managedTempFilePath != path) {
|
|
||||||
await cleanupTempFile(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isManagedTempFile(String path) {
|
|
||||||
return p.basename(path).startsWith('view_intent_') && p.basename(p.dirname(path)) == 'cache';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
|
||||||
import 'package:immich_mobile/models/view_intent/view_intent_payload.extension.dart';
|
|
||||||
import 'package:immich_mobile/platform/view_intent_api.g.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
class ViewIntentResolvedAsset {
|
|
||||||
final BaseAsset asset;
|
|
||||||
final TimelineService timelineService;
|
|
||||||
|
|
||||||
final String? viewIntentFilePath;
|
|
||||||
|
|
||||||
const ViewIntentResolvedAsset({required this.asset, required this.timelineService, this.viewIntentFilePath});
|
|
||||||
}
|
|
||||||
|
|
||||||
final viewIntentAssetResolverProvider = Provider<ViewIntentAssetResolver>(
|
|
||||||
(ref) => ViewIntentAssetResolver(
|
|
||||||
localAssetRepository: ref.read(localAssetRepository),
|
|
||||||
timelineFactory: ref.read(timelineFactoryProvider),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
class ViewIntentAssetResolver {
|
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
|
||||||
final TimelineFactory _timelineFactory;
|
|
||||||
static final Logger _logger = Logger('ViewIntentAssetResolver');
|
|
||||||
|
|
||||||
const ViewIntentAssetResolver({required this._localAssetRepository, required this._timelineFactory});
|
|
||||||
|
|
||||||
Future<ViewIntentResolvedAsset> resolve(ViewIntentPayload attachment) async {
|
|
||||||
final localAssetId = attachment.localAssetId;
|
|
||||||
final path = attachment.path;
|
|
||||||
_logger.fine('resolve start, localAssetId=$localAssetId, path=$path, mimeType=${attachment.mimeType}');
|
|
||||||
|
|
||||||
if (localAssetId == null && path == null) {
|
|
||||||
throw StateError('ViewIntent resolution requires either a localAssetId or a materialized file path.');
|
|
||||||
}
|
|
||||||
|
|
||||||
final localAsset = localAssetId != null ? await _localAssetRepository.getById(localAssetId) : null;
|
|
||||||
final asset = localAsset ?? _toTransientAsset(attachment);
|
|
||||||
|
|
||||||
return ViewIntentResolvedAsset(
|
|
||||||
asset: asset,
|
|
||||||
timelineService: _timelineFactory.fromAssets([asset], TimelineOrigin.deepLink),
|
|
||||||
viewIntentFilePath: localAsset == null ? path : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalAsset _toTransientAsset(ViewIntentPayload attachment) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
return LocalAsset(
|
|
||||||
id: attachment.localAssetId ?? '-${attachment.path!.hashCode.abs()}',
|
|
||||||
name: attachment.fileName,
|
|
||||||
type: attachment.isVideo ? AssetType.video : AssetType.image,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
isEdited: false,
|
|
||||||
playbackStyle: attachment.playbackStyle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user