Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fffe41f0b | |||
| 83afd49f5c | |||
| 639ede78c2 | |||
| 15be3437bf | |||
| f59b0bab5a | |||
| fa418d778b | |||
| e0c4b8df6f | |||
| 7f9689b4bc | |||
| e6f8bfdf5e | |||
| 8ccca04e27 | |||
| 53f80393bf | |||
| e5e857edc3 | |||
| 590f96246d | |||
| 38d73f2bc6 | |||
| 96e3b96d57 | |||
| 36b018e355 | |||
| 214ca50406 | |||
| 29b3981609 | |||
| a068a41c06 | |||
| 3c6e9e1191 | |||
| db0415bbcc | |||
| a5c431fbf5 | |||
| a3d588f6bd | |||
| 21f500191a | |||
| 5011636d95 | |||
| 3f330c6476 | |||
| bb8755021d | |||
| 93f9e118ad | |||
| 58ca1402ed | |||
| 32a7087883 | |||
| 53020852ec | |||
| 181a7e115f | |||
| 095ace8687 | |||
| 4c3fcdc745 | |||
| fa5f30d9ca | |||
| e60bc3c304 |
@@ -73,10 +73,8 @@ install_dependencies() {
|
|||||||
log "Installing dependencies"
|
log "Installing dependencies"
|
||||||
(
|
(
|
||||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||||
run_cmd make ci-server
|
export CI=1 FROZEN=1 OFFLINE=1
|
||||||
run_cmd make ci-sdk
|
run_cmd make setup-dev
|
||||||
run_cmd make build-sdk
|
|
||||||
run_cmd make ci-web
|
|
||||||
)
|
)
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ services:
|
|||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|
||||||
database:
|
database:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
environment: !override
|
environment: !override
|
||||||
@@ -31,7 +31,7 @@ services:
|
|||||||
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
||||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||||
POSTGRES_HOST_AUTH_METHOD: md5
|
POSTGRES_HOST_AUTH_METHOD: md5
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ cd "${IMMICH_WORKSPACE}/server" || (
|
|||||||
exit 1
|
exit 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CI=1 pnpm install
|
||||||
while true; do
|
while true; do
|
||||||
run_cmd node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch
|
run_cmd pnpm exec nest start --debug "0.0.0.0:9230" --watch
|
||||||
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
|
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_
|
|||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
run_cmd node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
run_cmd pnpm exec vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
||||||
log "Web crashed with exit code $?. Respawning in 3s ..."
|
log "Web crashed with exit code $?. Respawning in 3s ..."
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
|
Dockerfile
|
||||||
!docker/scripts
|
!docker/scripts
|
||||||
docs/
|
docs/
|
||||||
!docs/package.json
|
!docs/package.json
|
||||||
@@ -19,6 +20,7 @@ mobile/
|
|||||||
cli/coverage/
|
cli/coverage/
|
||||||
cli/dist/
|
cli/dist/
|
||||||
cli/node_modules/
|
cli/node_modules/
|
||||||
|
cli/Dockerfile
|
||||||
|
|
||||||
open-api/typescript-sdk/build/
|
open-api/typescript-sdk/build/
|
||||||
open-api/typescript-sdk/node_modules/
|
open-api/typescript-sdk/node_modules/
|
||||||
@@ -29,9 +31,11 @@ server/upload/
|
|||||||
server/src/queries
|
server/src/queries
|
||||||
server/dist/
|
server/dist/
|
||||||
server/www/
|
server/www/
|
||||||
|
server/Dockerfile
|
||||||
|
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/coverage/
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
web/build/
|
||||||
web/.env
|
web/.env
|
||||||
|
web/Dockerfile
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.17.0
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
Generated
-28
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": ".github",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier": "^3.5.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "3.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
|
||||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin/prettier.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -66,12 +66,6 @@ jobs:
|
|||||||
ref: ${{ inputs.ref || github.sha }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install missing deps
|
|
||||||
run: |
|
|
||||||
sudo add-apt-repository ppa:rmescandon/yq
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y yq xz-utils ninja-build zstd
|
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
env:
|
env:
|
||||||
KEY_JKS: ${{ secrets.KEY_JKS }}
|
KEY_JKS: ${{ secrets.KEY_JKS }}
|
||||||
@@ -96,7 +90,7 @@ jobs:
|
|||||||
key: build-mobile-gradle-${{ runner.os }}-main
|
key: build-mobile-gradle-${{ runner.os }}-main
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
|
|||||||
+17
-14
@@ -33,21 +33,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
- name: Setup pnpm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
with:
|
|
||||||
node-version-file: './cli/.nvmrc'
|
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
cache: 'npm'
|
|
||||||
cache-dependency-path: '**/package-lock.json'
|
|
||||||
|
|
||||||
- name: Prepare SDK
|
- name: Setup Node
|
||||||
run: npm ci --prefix ../open-api/typescript-sdk/
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
- name: Build SDK
|
with:
|
||||||
run: npm run build --prefix ../open-api/typescript-sdk/
|
node-version-file: './server/.nvmrc'
|
||||||
- run: npm ci
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- run: npm run build
|
cache: 'pnpm'
|
||||||
- run: npm publish
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
|
- name: Setup typescript-sdk
|
||||||
|
run: pnpm install && pnpm run build
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm build
|
||||||
|
- run: pnpm publish
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -53,21 +53,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './docs/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run install
|
||||||
run: npm ci
|
run: pnpm install
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: npm run format
|
run: pnpm format
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: npm run build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Upload build output
|
- name: Upload build output
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Fix formatting
|
- name: Fix formatting
|
||||||
run: make install-all && make format-all
|
run: make install-all && make format-all
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Require PR to have a changelog label
|
- name: Require PR to have a changelog label
|
||||||
uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
|
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
|
||||||
with:
|
with:
|
||||||
mode: exactly
|
mode: exactly
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -20,18 +20,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: pnpm build
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: npm publish
|
run: pnpm publish
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
|
|||||||
+130
-86
@@ -80,30 +80,33 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run package manager install
|
||||||
run: npm ci
|
run: pnpm install
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: pnpm lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: npm run format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run small tests & coverage
|
- name: Run small tests & coverage
|
||||||
run: npm test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
@@ -123,34 +126,37 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install && pnpm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: npm ci
|
run: pnpm install
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: pnpm lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: npm run format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: npm run test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests-win:
|
cli-unit-tests-win:
|
||||||
@@ -170,27 +176,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Skip linter & formatter in Windows test.
|
# Skip linter & formatter in Windows test.
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: npm run test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-lint:
|
web-lint:
|
||||||
@@ -210,30 +219,33 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: npm ci
|
run: pnpm rebuild && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint:p
|
run: pnpm lint:p
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: npm run format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run svelte checks
|
- name: Run svelte checks
|
||||||
run: npm run check:svelte
|
run: pnpm check:svelte
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
@@ -253,26 +265,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run check:typescript
|
run: pnpm check:typescript
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: npm run test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
i18n-tests:
|
i18n-tests:
|
||||||
@@ -288,18 +303,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm --prefix=web ci
|
run: pnpm --filter=immich-web install --frozen-lockfile
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: npm --prefix=web run format:i18n
|
run: pnpm --filter=immich-web format:i18n
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -334,32 +352,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: pnpm lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: npm run format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run check
|
run: pnpm check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
server-medium-tests:
|
server-medium-tests:
|
||||||
@@ -379,18 +400,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
run: npm run test:medium
|
run: pnpm test:medium
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-server-cli:
|
e2e-tests-server-cli:
|
||||||
@@ -414,25 +438,33 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run setup web
|
||||||
|
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
||||||
|
working-directory: ./web
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run setup cli
|
- name: Run setup cli
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
@@ -440,7 +472,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: npm run test
|
run: pnpm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-web:
|
e2e-tests-web:
|
||||||
@@ -464,20 +496,23 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: pnpm install --frozen-lockfile && pnpm build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
@@ -516,7 +551,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -584,18 +619,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './.github/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: npm run format
|
run: pnpm format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
shellcheck:
|
shellcheck:
|
||||||
@@ -627,18 +665,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: npm --prefix=server ci
|
run: pnpm --filter immich install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: npm --prefix=server run build
|
run: pnpm --filter immich build
|
||||||
|
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
@@ -690,28 +731,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: npm run build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: npm run migrations:run
|
run: pnpm migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: npm run schema:reset
|
run: pnpm schema:reset
|
||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: npm run migrations:generate src/TestMigration
|
run: pnpm migrations:generate src/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||||
@@ -730,7 +774,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
run: npm run sync:sql
|
run: pnpm sync:sql
|
||||||
env:
|
env:
|
||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
dev:
|
dev:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-down:
|
dev-down:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
e2e-update:
|
||||||
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
e2e-down:
|
||||||
|
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-down:
|
prod-down:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
.PHONY: open-api
|
.PHONY: open-api
|
||||||
open-api:
|
open-api:
|
||||||
@@ -34,7 +40,7 @@ open-api-typescript:
|
|||||||
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
npm --prefix server run sync:sql
|
pnpm --filter immich run sync:sql
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
@@ -44,31 +50,40 @@ renovate:
|
|||||||
|
|
||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
|
# directory to package name mapping function
|
||||||
|
# cli = @immich/cli
|
||||||
|
# docs = documentation
|
||||||
|
# e2e = immich-e2e
|
||||||
|
# open-api/typescript-sdk = @immich/sdk
|
||||||
|
# server = immich
|
||||||
|
# web = immich-web
|
||||||
|
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
|
||||||
|
|
||||||
audit-%:
|
audit-%:
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
pnpm --filter $(call map-package,$*) audit fix
|
||||||
install-%:
|
install-%:
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
|
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
|
||||||
ci-%:
|
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci
|
|
||||||
build-cli: build-sdk
|
build-cli: build-sdk
|
||||||
build-web: build-sdk
|
build-web: build-sdk
|
||||||
build-%: install-%
|
build-%: install-%
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
|
pnpm --filter $(call map-package,$*) run build
|
||||||
format-%:
|
format-%:
|
||||||
npm --prefix $* run format:fix
|
pnpm --filter $(call map-package,$*) run format:fix
|
||||||
lint-%:
|
lint-%:
|
||||||
npm --prefix $* run lint:fix
|
pnpm --filter $(call map-package,$*) run lint:fix
|
||||||
|
lint-web:
|
||||||
|
pnpm --filter $(call map-package,$*) run lint:p
|
||||||
check-%:
|
check-%:
|
||||||
npm --prefix $* run check
|
pnpm --filter $(call map-package,$*) run check
|
||||||
check-web:
|
check-web:
|
||||||
npm --prefix web run check:typescript
|
pnpm --filter immich-web run check:typescript
|
||||||
npm --prefix web run check:svelte
|
pnpm --filter immich-web run check:svelte
|
||||||
test-%:
|
test-%:
|
||||||
npm --prefix $* run test
|
pnpm --filter $(call map-package,$*) run test
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
docker compose -f ./e2e/docker-compose.yml build
|
||||||
npm --prefix e2e run test
|
pnpm --filter immich-e2e run test
|
||||||
npm --prefix e2e run test:web
|
pnpm --filter immich-e2e run test:web
|
||||||
test-medium:
|
test-medium:
|
||||||
docker run \
|
docker run \
|
||||||
--rm \
|
--rm \
|
||||||
@@ -78,24 +93,34 @@ test-medium:
|
|||||||
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
|
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
|
||||||
-e NODE_ENV=development \
|
-e NODE_ENV=development \
|
||||||
immich-server:latest \
|
immich-server:latest \
|
||||||
-c "npm ci && npm run test:medium -- --run"
|
-c "pnpm test:medium -- --run"
|
||||||
test-medium-dev:
|
test-medium-dev:
|
||||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
|
||||||
|
|
||||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
install-all:
|
||||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
pnpm -r --filter '!documentation' install
|
||||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
|
||||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
|
||||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
|
||||||
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
check-all:
|
||||||
audit-all: $(foreach M,$(MODULES),audit-$M) ;
|
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
|
||||||
hygiene-all: lint-all format-all check-all sql audit-all;
|
lint-all:
|
||||||
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
|
pnpm -r --filter '!documentation' run lint:fix
|
||||||
|
format-all:
|
||||||
|
pnpm -r --filter '!documentation' run format:fix
|
||||||
|
audit-all:
|
||||||
|
pnpm -r --filter '!documentation' audit fix
|
||||||
|
hygiene-all: audit-all
|
||||||
|
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
pnpm -r --filter '!documentation' run "/^test/"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||||
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||||
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||||
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
|
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||||
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||||
docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||||
|
setup-dev: install-server install-sdk build-sdk install-web
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.17.0
|
||||||
|
|||||||
+32
-10
@@ -1,19 +1,41 @@
|
|||||||
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
|
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
ENV COREPACK_ENABLE_AUTO_PIN=0 \
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||||
RUN npm ci
|
|
||||||
COPY open-api/typescript-sdk/ ./
|
RUN corepack enable && \
|
||||||
RUN npm run build
|
corepack install -g pnpm
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
COPY cli/package.json cli/package-lock.json ./
|
WORKDIR /usr/src/app/web
|
||||||
RUN npm ci
|
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
|
||||||
|
pnpm install --frozen-lockfile && \
|
||||||
|
pnpm exec svelte-kit sync
|
||||||
|
|
||||||
COPY cli .
|
WORKDIR /usr/src/app/open-api/typescript-sdk
|
||||||
RUN npm run build
|
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
|
||||||
|
pnpm install --frozen-lockfile && \
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app/cli
|
||||||
|
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
|
||||||
|
pnpm install --frozen-lockfile --prod --no-optional && \
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
RUN rm -rf /usr/src/app/web && \
|
||||||
|
rm -rf /usr/src/app/open-api && \
|
||||||
|
rm -rf /usr/src/app/cli/src && \
|
||||||
|
rm -rf /usr/src/app/cli/src && \
|
||||||
|
rm -rf /usr/src/app/server && \
|
||||||
|
rm -rf /usr/src/app/i18n && \
|
||||||
|
rm -rf /usr/src/app/e2e && \
|
||||||
|
rm -rf /usr/src/app/docs && \
|
||||||
|
rm -rf /usr/src/app/readme_i18n && \
|
||||||
|
rm -rf /usr/src/app/deployment && \
|
||||||
|
rm -rf /usr/src/app/docker
|
||||||
|
|
||||||
WORKDIR /import
|
WORKDIR /import
|
||||||
|
|
||||||
ENTRYPOINT ["node", "/usr/src/app/dist"]
|
ENTRYPOINT ["node", "/usr/src/app/cli/dist"]
|
||||||
|
|||||||
+8
-4
@@ -6,8 +6,10 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
|
|||||||
|
|
||||||
Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
|
Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
|
||||||
|
|
||||||
$ npm install
|
# if you don't have node installed
|
||||||
$ npm run build
|
$ npm install -g pnpm
|
||||||
|
$ pnpm install
|
||||||
|
$ pnpm build
|
||||||
|
|
||||||
Then, to build the open-api client run the following in the open-api folder:
|
Then, to build the open-api client run the following in the open-api folder:
|
||||||
|
|
||||||
@@ -15,8 +17,10 @@ Then, to build the open-api client run the following in the open-api folder:
|
|||||||
|
|
||||||
To run the Immich CLI from source, run the following in the cli folder:
|
To run the Immich CLI from source, run the following in the cli folder:
|
||||||
|
|
||||||
$ npm install
|
# if you don't have node installed
|
||||||
$ npm run build
|
$ npm install -g pnpm
|
||||||
|
$ pnpm install
|
||||||
|
$ pnpm build
|
||||||
$ ts-node .
|
$ ts-node .
|
||||||
|
|
||||||
You'll need ts-node, the easiest way to install it is to use npm:
|
You'll need ts-node, the easiest way to install it is to use npm:
|
||||||
|
|||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import '../dist/index.js';
|
||||||
Generated
-4517
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"immich": "dist/index.js"
|
"immich": "./bin/immich"
|
||||||
},
|
},
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.15.32",
|
"@types/node": "^22.15.33",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -69,6 +69,6 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.16.0"
|
"node": "22.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ name: immich-dev
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['/usr/src/app/bin/immich-dev']
|
command: ['/usr/src/app/server/bin/immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -24,13 +24,12 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
target: dev
|
target: dev-docker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ..:/usr/src/app
|
||||||
- ../open-api:/usr/src/open-api
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/server/upload
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/server/upload/upload
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
@@ -69,17 +68,16 @@ services:
|
|||||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
||||||
# user: 0:0
|
# user: 0:0
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../
|
||||||
command: ['/usr/src/app/bin/immich-web']
|
dockerfile: web/Dockerfile
|
||||||
|
command: ['/usr/src/app/web/bin/immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ..:/usr/src/app
|
||||||
- ../i18n:/usr/src/i18n
|
|
||||||
- ../open-api/:/usr/src/open-api/
|
|
||||||
# - ../../ui:/usr/ui
|
# - ../../ui:/usr/ui
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
ulimits:
|
ulimits:
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
|
image: prom/prometheus@sha256:7a34573f0b9c952286b33d537f233cd5b708e12263733aa646e50c33f598f16c
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.17.0
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
build/
|
build/
|
||||||
.docusaurus/
|
.docusaurus/
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm install
|
$ pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|||||||
@@ -150,12 +150,10 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
|
|||||||
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
|
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
|
||||||
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
|
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
|
||||||
- **Encoded Assets:**
|
- **Encoded Assets:**
|
||||||
|
|
||||||
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
|
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
|
||||||
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
|
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
|
||||||
|
|
||||||
- **Postgres**
|
- **Postgres**
|
||||||
|
|
||||||
- The Immich database containing all the information to allow the system to function properly.
|
- The Immich database containing all the information to allow the system to function properly.
|
||||||
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
|
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
|
||||||
- Stored in `DB_DATA_LOCATION`.
|
- Stored in `DB_DATA_LOCATION`.
|
||||||
@@ -201,7 +199,6 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
|
|||||||
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
|
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
|
||||||
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
|
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
|
||||||
- **Postgres**
|
- **Postgres**
|
||||||
|
|
||||||
- The Immich database containing all the information to allow the system to function properly.
|
- The Immich database containing all the information to allow the system to function properly.
|
||||||
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
|
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
|
||||||
- Stored in `DB_DATA_LOCATION`.
|
- Stored in `DB_DATA_LOCATION`.
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an i
|
|||||||
Before enabling OAuth in Immich, a new client application needs to be configured in the 3rd-party authentication server. While the specifics of this setup vary from provider to provider, the general approach should be the same.
|
Before enabling OAuth in Immich, a new client application needs to be configured in the 3rd-party authentication server. While the specifics of this setup vary from provider to provider, the general approach should be the same.
|
||||||
|
|
||||||
1. Create a new (Client) Application
|
1. Create a new (Client) Application
|
||||||
|
|
||||||
1. The **Provider** type should be `OpenID Connect` or `OAuth2`
|
1. The **Provider** type should be `OpenID Connect` or `OAuth2`
|
||||||
2. The **Client type** should be `Confidential`
|
2. The **Client type** should be `Confidential`
|
||||||
3. The **Application** type should be `Web`
|
3. The **Application** type should be `Web`
|
||||||
@@ -29,7 +28,6 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
|||||||
2. Configure Redirect URIs/Origins
|
2. Configure Redirect URIs/Origins
|
||||||
|
|
||||||
The **Sign-in redirect URIs** should include:
|
The **Sign-in redirect URIs** should include:
|
||||||
|
|
||||||
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
|
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
|
||||||
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
|
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
|
||||||
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
|
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
|
||||||
@@ -37,21 +35,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
|||||||
Redirect URIs should contain all the domains you will be using to access Immich. Some examples include:
|
Redirect URIs should contain all the domains you will be using to access Immich. Some examples include:
|
||||||
|
|
||||||
Mobile
|
Mobile
|
||||||
|
|
||||||
- `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
|
- `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
|
||||||
|
|
||||||
Localhost
|
Localhost
|
||||||
|
|
||||||
- `http://localhost:2283/auth/login`
|
- `http://localhost:2283/auth/login`
|
||||||
- `http://localhost:2283/user-settings`
|
- `http://localhost:2283/user-settings`
|
||||||
|
|
||||||
Local IP
|
Local IP
|
||||||
|
|
||||||
- `http://192.168.0.200:2283/auth/login`
|
- `http://192.168.0.200:2283/auth/login`
|
||||||
- `http://192.168.0.200:2283/user-settings`
|
- `http://192.168.0.200:2283/user-settings`
|
||||||
|
|
||||||
Hostname
|
Hostname
|
||||||
|
|
||||||
- `https://immich.example.com/auth/login`
|
- `https://immich.example.com/auth/login`
|
||||||
- `https://immich.example.com/user-settings`
|
- `https://immich.example.com/user-settings`
|
||||||
|
|
||||||
|
|||||||
@@ -199,13 +199,11 @@ To use your SSH key for commit signing, see the [GitHub guide on SSH commit sign
|
|||||||
When the Dev Container starts, it automatically:
|
When the Dev Container starts, it automatically:
|
||||||
|
|
||||||
1. **Runs post-create script** (`container-server-post-create.sh`):
|
1. **Runs post-create script** (`container-server-post-create.sh`):
|
||||||
|
|
||||||
- Adjusts file permissions for the `node` user
|
- Adjusts file permissions for the `node` user
|
||||||
- Installs dependencies: `npm install` in all packages
|
- Installs dependencies: `pnpm install` in all packages
|
||||||
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
|
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
|
||||||
|
|
||||||
2. **Starts development servers** via VS Code tasks:
|
2. **Starts development servers** via VS Code tasks:
|
||||||
|
|
||||||
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
||||||
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
|
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
|
||||||
- Both servers watch for file changes and recompile automatically
|
- Both servers watch for file changes and recompile automatically
|
||||||
@@ -335,14 +333,12 @@ make install-all # Install all dependencies
|
|||||||
The Dev Container is pre-configured for debugging:
|
The Dev Container is pre-configured for debugging:
|
||||||
|
|
||||||
1. **API Server Debugging**:
|
1. **API Server Debugging**:
|
||||||
|
|
||||||
- Set breakpoints in VS Code
|
- Set breakpoints in VS Code
|
||||||
- Press `F5` or use "Run and Debug" panel
|
- Press `F5` or use "Run and Debug" panel
|
||||||
- Select "Attach to Server" configuration
|
- Select "Attach to Server" configuration
|
||||||
- Debug port: 9231
|
- Debug port: 9231
|
||||||
|
|
||||||
2. **Worker Debugging**:
|
2. **Worker Debugging**:
|
||||||
|
|
||||||
- Use "Attach to Workers" configuration
|
- Use "Attach to Workers" configuration
|
||||||
- Debug port: 9230
|
- Debug port: 9230
|
||||||
|
|
||||||
@@ -428,7 +424,6 @@ While the Dev Container focuses on server and web development, you can connect m
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Configure mobile app**:
|
2. **Configure mobile app**:
|
||||||
|
|
||||||
- Server URL: `http://YOUR_IP:2283/api`
|
- Server URL: `http://YOUR_IP:2283/api`
|
||||||
- Ensure firewall allows port 2283
|
- Ensure firewall allows port 2283
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ If you only want to do web development connected to an existing, remote backend,
|
|||||||
|
|
||||||
1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -`
|
1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -`
|
||||||
2. Enter the web directory - `cd web/`
|
2. Enter the web directory - `cd web/`
|
||||||
3. Install web dependencies - `npm i`
|
3. Install web dependencies - `pnpm i`
|
||||||
4. Start the web development server
|
4. Start the web development server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
### Unit tests
|
### Unit tests
|
||||||
|
|
||||||
Unit are run by calling `npm run test` from the `server/` directory.
|
Unit are run by calling `npm run test` from the `server/` directory.
|
||||||
You need to run `npm install` (in `server/`) before _once_.
|
You need to run `pnpm install` (in `server/`) before _once_.
|
||||||
|
|
||||||
### End to end tests
|
### End to end tests
|
||||||
|
|
||||||
|
|||||||
@@ -72,22 +72,25 @@ Information on the current workers can be found [here](/docs/administration/jobs
|
|||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
||||||
| `DB_URL` | Database URL | | server |
|
| `DB_URL` | Database URL | | server |
|
||||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||||
| `DB_PORT` | Database port | `5432` | server |
|
| `DB_PORT` | Database port | `5432` | server |
|
||||||
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
||||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||||
|
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | server |
|
||||||
|
|
||||||
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
||||||
|
|
||||||
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
|
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
|
||||||
|
|
||||||
|
\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
|||||||
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
||||||
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
||||||
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
||||||
|
|
||||||
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
|
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
|
||||||
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
|
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
|
||||||
|
|
||||||
|
|||||||
Generated
-20954
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -16,8 +16,9 @@
|
|||||||
"write-heading-ids": "docusaurus write-heading-ids"
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "~3.7.0",
|
"@docusaurus/core": "~3.8.0",
|
||||||
"@docusaurus/preset-classic": "~3.7.0",
|
"@docusaurus/preset-classic": "~3.8.0",
|
||||||
|
"@docusaurus/theme-common": "~3.8.0",
|
||||||
"@mdi/js": "^7.3.67",
|
"@mdi/js": "^7.3.67",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"docusaurus-lunr-search": "^3.3.2",
|
"docusaurus-lunr-search": "^3.3.2",
|
||||||
"docusaurus-preset-openapi": "^0.7.5",
|
"docusaurus-preset-openapi": "^0.7.5",
|
||||||
|
"lunr": "^2.3.9",
|
||||||
"postcss": "^8.4.25",
|
"postcss": "^8.4.25",
|
||||||
"prism-react-renderer": "^2.3.1",
|
"prism-react-renderer": "^2.3.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "~3.7.0",
|
"@docusaurus/module-type-aliases": "~3.8.0",
|
||||||
"@docusaurus/tsconfig": "^3.7.0",
|
"@docusaurus/tsconfig": "^3.7.0",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
@@ -57,6 +59,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.16.0"
|
"node": "22.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ const guides: CommunityGuidesProps[] = [
|
|||||||
description: 'Access Immich with an end-to-end encrypted connection.',
|
description: 'Access Immich with an end-to-end encrypted connection.',
|
||||||
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access',
|
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Trust Self Signed Certificates with Immich - OAuth Setup',
|
||||||
|
description:
|
||||||
|
'Set up Certificate Authority trust with Immich, and your private OAuth2/OpenID service, while using a private CA for HTTPS commication.',
|
||||||
|
url: 'https://github.com/immich-app/immich/discussions/18614',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.17.0
|
||||||
|
|||||||
Generated
-7348
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -25,7 +25,7 @@
|
|||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.15.32",
|
"@types/node": "^22.15.33",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.34.0",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
@@ -53,6 +53,6 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.16.0"
|
"node": "22.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,13 @@ describe('/shared-links', () => {
|
|||||||
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
||||||
expect(resp.status).toBe(200);
|
expect(resp.status).toBe(200);
|
||||||
expect(resp.header['content-type']).toContain('text/html');
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
|
expect(resp.text).toContain(`<meta property="og:image" content="http://127.0.0.1:2285`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
|
||||||
|
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`).set('Host', '');
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
|
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
import {
|
|
||||||
AssetMediaResponseDto,
|
|
||||||
AssetVisibility,
|
|
||||||
LoginResponseDto,
|
|
||||||
SharedLinkType,
|
|
||||||
TimeBucketAssetResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { createUserDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, utils } from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
// TODO this should probably be a test util function
|
|
||||||
const today = DateTime.fromObject({
|
|
||||||
year: 2023,
|
|
||||||
month: 11,
|
|
||||||
day: 3,
|
|
||||||
}) as DateTime<true>;
|
|
||||||
const yesterday = today.minus({ days: 1 });
|
|
||||||
|
|
||||||
describe('/timeline', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let user: LoginResponseDto;
|
|
||||||
let timeBucketUser: LoginResponseDto;
|
|
||||||
|
|
||||||
let user1Assets: AssetMediaResponseDto[];
|
|
||||||
let user2Assets: AssetMediaResponseDto[];
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
admin = await utils.adminSetup({ onboarding: false });
|
|
||||||
[user, timeBucketUser] = await Promise.all([
|
|
||||||
utils.userSetup(admin.accessToken, createUserDto.create('1')),
|
|
||||||
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
|
||||||
]);
|
|
||||||
|
|
||||||
user1Assets = await Promise.all([
|
|
||||||
utils.createAsset(user.accessToken),
|
|
||||||
utils.createAsset(user.accessToken),
|
|
||||||
utils.createAsset(user.accessToken, {
|
|
||||||
isFavorite: true,
|
|
||||||
fileCreatedAt: yesterday.toISO(),
|
|
||||||
fileModifiedAt: yesterday.toISO(),
|
|
||||||
assetData: { filename: 'example.mp4' },
|
|
||||||
}),
|
|
||||||
utils.createAsset(user.accessToken),
|
|
||||||
utils.createAsset(user.accessToken),
|
|
||||||
]);
|
|
||||||
|
|
||||||
user2Assets = await Promise.all([
|
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /timeline/buckets', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/timeline/buckets');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get time buckets by month', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{ count: 3, timeBucket: '1970-02-01' },
|
|
||||||
{ count: 1, timeBucket: '1970-01-01' },
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow access for unrelated shared links', async () => {
|
|
||||||
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: user1Assets.map(({ id }) => id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
|
||||||
const req1 = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ withPartners: true, visibility: AssetVisibility.Archive });
|
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
|
||||||
|
|
||||||
const req2 = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
|
||||||
.query({ withPartners: true, visibility: undefined });
|
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
|
||||||
const req1 = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ withPartners: true, isFavorite: true });
|
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
|
||||||
|
|
||||||
const req2 = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ withPartners: true, isFavorite: false });
|
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
|
||||||
const req = await request(app)
|
|
||||||
.get('/timeline/buckets')
|
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
|
||||||
.query({ withPartners: true, isTrashed: true });
|
|
||||||
|
|
||||||
expect(req.status).toBe(400);
|
|
||||||
expect(req.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /timeline/bucket', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
|
||||||
timeBucket: '1900-01-01',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle 5 digit years', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/timeline/bucket')
|
|
||||||
.query({ timeBucket: '012345-01-01' })
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({
|
|
||||||
city: [],
|
|
||||||
country: [],
|
|
||||||
duration: [],
|
|
||||||
id: [],
|
|
||||||
visibility: [],
|
|
||||||
isFavorite: [],
|
|
||||||
isImage: [],
|
|
||||||
isTrashed: [],
|
|
||||||
livePhotoVideoId: [],
|
|
||||||
fileCreatedAt: [],
|
|
||||||
localOffsetHours: [],
|
|
||||||
ownerId: [],
|
|
||||||
projectionType: [],
|
|
||||||
ratio: [],
|
|
||||||
status: [],
|
|
||||||
thumbhash: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO enable date string validation while still accepting 5 digit years
|
|
||||||
// it('should fail if time bucket is invalid', async () => {
|
|
||||||
// const { status, body } = await request(app)
|
|
||||||
// .get('/timeline/bucket')
|
|
||||||
// .set('Authorization', `Bearer ${user.accessToken}`)
|
|
||||||
// .query({ timeBucket: 'foo' });
|
|
||||||
|
|
||||||
// expect(status).toBe(400);
|
|
||||||
// expect(body).toEqual(errorDto.badRequest);
|
|
||||||
// });
|
|
||||||
|
|
||||||
it('should return time bucket', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/timeline/bucket')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ timeBucket: '1970-02-10' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({
|
|
||||||
city: [],
|
|
||||||
country: [],
|
|
||||||
duration: [],
|
|
||||||
id: [],
|
|
||||||
visibility: [],
|
|
||||||
isFavorite: [],
|
|
||||||
isImage: [],
|
|
||||||
isTrashed: [],
|
|
||||||
livePhotoVideoId: [],
|
|
||||||
fileCreatedAt: [],
|
|
||||||
localOffsetHours: [],
|
|
||||||
ownerId: [],
|
|
||||||
projectionType: [],
|
|
||||||
ratio: [],
|
|
||||||
status: [],
|
|
||||||
thumbhash: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return time bucket in trash', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/timeline/bucket')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const timeBucket: TimeBucketAssetResponseDto = body;
|
|
||||||
expect(timeBucket.isTrashed).toEqual([true]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+4
-3
@@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client';
|
|||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
export type { Emitter } from '@socket.io/component-emitter';
|
||||||
|
|
||||||
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
||||||
@@ -78,16 +79,16 @@ export const tempDir = tmpdir();
|
|||||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
export const immichCli = (args: string[]) =>
|
export const immichCli = (args: string[]) =>
|
||||||
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
|
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
||||||
export const immichAdmin = (args: string[]) =>
|
export const immichAdmin = (args: string[]) =>
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
const executeCommand = (command: string, args: string[]) => {
|
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
|
||||||
let _resolve: (value: CommandResponse) => void;
|
let _resolve: (value: CommandResponse) => void;
|
||||||
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||||
const child = spawn(command, args, { stdio: 'pipe' });
|
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
|
|||||||
+6
-1
@@ -427,6 +427,7 @@
|
|||||||
"app_settings": "App Settings",
|
"app_settings": "App Settings",
|
||||||
"appears_in": "Appears in",
|
"appears_in": "Appears in",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
|
"archive_action_prompt": "{count} added to Archive",
|
||||||
"archive_or_unarchive_photo": "Archive or unarchive photo",
|
"archive_or_unarchive_photo": "Archive or unarchive photo",
|
||||||
"archive_page_no_archived_assets": "No archived assets found",
|
"archive_page_no_archived_assets": "No archived assets found",
|
||||||
"archive_page_title": "Archive ({count})",
|
"archive_page_title": "Archive ({count})",
|
||||||
@@ -702,7 +703,7 @@
|
|||||||
"daily_title_text_date": "E, MMM dd",
|
"daily_title_text_date": "E, MMM dd",
|
||||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"darkTheme": "Toggle dark theme",
|
"dark_theme": "Toggle dark theme",
|
||||||
"date_after": "Date after",
|
"date_after": "Date after",
|
||||||
"date_and_time": "Date and Time",
|
"date_and_time": "Date and Time",
|
||||||
"date_before": "Date before",
|
"date_before": "Date before",
|
||||||
@@ -798,6 +799,7 @@
|
|||||||
"edit_key": "Edit key",
|
"edit_key": "Edit key",
|
||||||
"edit_link": "Edit link",
|
"edit_link": "Edit link",
|
||||||
"edit_location": "Edit location",
|
"edit_location": "Edit location",
|
||||||
|
"edit_location_action_prompt": "{count} location edited",
|
||||||
"edit_location_dialog_title": "Location",
|
"edit_location_dialog_title": "Location",
|
||||||
"edit_name": "Edit name",
|
"edit_name": "Edit name",
|
||||||
"edit_people": "Edit people",
|
"edit_people": "Edit people",
|
||||||
@@ -983,6 +985,7 @@
|
|||||||
"failed_to_load_assets": "Failed to load assets",
|
"failed_to_load_assets": "Failed to load assets",
|
||||||
"failed_to_load_folder": "Failed to load folder",
|
"failed_to_load_folder": "Failed to load folder",
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
|
"favorite_action_prompt": "{count} added to Favorites",
|
||||||
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
|
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
"favorites_page_no_favorites": "No favorite assets found",
|
"favorites_page_no_favorites": "No favorite assets found",
|
||||||
@@ -1245,6 +1248,7 @@
|
|||||||
"more": "More",
|
"more": "More",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
"move_off_locked_folder": "Move out of locked folder",
|
"move_off_locked_folder": "Move out of locked folder",
|
||||||
|
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
||||||
"move_to_locked_folder": "Move to locked folder",
|
"move_to_locked_folder": "Move to locked folder",
|
||||||
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
||||||
"moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
|
"moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
|
||||||
@@ -1495,6 +1499,7 @@
|
|||||||
"remove_deleted_assets": "Remove Deleted Assets",
|
"remove_deleted_assets": "Remove Deleted Assets",
|
||||||
"remove_from_album": "Remove from album",
|
"remove_from_album": "Remove from album",
|
||||||
"remove_from_favorites": "Remove from favorites",
|
"remove_from_favorites": "Remove from favorites",
|
||||||
|
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
|
||||||
"remove_from_locked_folder": "Remove from locked folder",
|
"remove_from_locked_folder": "Remove from locked folder",
|
||||||
"remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library.",
|
"remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of the locked folder? They will be visible in your library.",
|
||||||
"remove_from_shared_link": "Remove from shared link",
|
"remove_from_shared_link": "Remove from shared link",
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -12,3 +12,5 @@ enum TextSearchType {
|
|||||||
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
||||||
|
|
||||||
enum SortUserBy { id }
|
enum SortUserBy { id }
|
||||||
|
|
||||||
|
enum ActionSource { timeline, viewer }
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class Album {
|
|||||||
final String? thumbnailAssetId;
|
final String? thumbnailAssetId;
|
||||||
final bool isActivityEnabled;
|
final bool isActivityEnabled;
|
||||||
final AlbumAssetOrder order;
|
final AlbumAssetOrder order;
|
||||||
|
final int assetCount;
|
||||||
|
final String ownerName;
|
||||||
|
|
||||||
const Album({
|
const Album({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -32,20 +34,24 @@ class Album {
|
|||||||
this.thumbnailAssetId,
|
this.thumbnailAssetId,
|
||||||
required this.isActivityEnabled,
|
required this.isActivityEnabled,
|
||||||
required this.order,
|
required this.order,
|
||||||
|
required this.assetCount,
|
||||||
|
required this.ownerName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Album {
|
return '''Album {
|
||||||
id: $id,
|
id: $id,
|
||||||
name: $name,
|
name: $name,
|
||||||
ownerId: $ownerId,
|
ownerId: $ownerId,
|
||||||
description: $description,
|
description: $description,
|
||||||
createdAt: $createdAt,
|
createdAt: $createdAt,
|
||||||
updatedAt: $updatedAt,
|
updatedAt: $updatedAt,
|
||||||
isActivityEnabled: $isActivityEnabled,
|
isActivityEnabled: $isActivityEnabled,
|
||||||
order: $order,
|
order: $order,
|
||||||
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
thumbnailAssetId: ${thumbnailAssetId ?? "<NA>"}
|
||||||
|
assetCount: $assetCount
|
||||||
|
ownerName: $ownerName
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +67,9 @@ class Album {
|
|||||||
updatedAt == other.updatedAt &&
|
updatedAt == other.updatedAt &&
|
||||||
thumbnailAssetId == other.thumbnailAssetId &&
|
thumbnailAssetId == other.thumbnailAssetId &&
|
||||||
isActivityEnabled == other.isActivityEnabled &&
|
isActivityEnabled == other.isActivityEnabled &&
|
||||||
order == other.order;
|
order == other.order &&
|
||||||
|
assetCount == other.assetCount &&
|
||||||
|
ownerName == other.ownerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -74,6 +82,8 @@ class Album {
|
|||||||
updatedAt.hashCode ^
|
updatedAt.hashCode ^
|
||||||
thumbnailAssetId.hashCode ^
|
thumbnailAssetId.hashCode ^
|
||||||
isActivityEnabled.hashCode ^
|
isActivityEnabled.hashCode ^
|
||||||
order.hashCode;
|
order.hashCode ^
|
||||||
|
assetCount.hashCode ^
|
||||||
|
ownerName.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
part 'asset.model.dart';
|
part 'remote_asset.model.dart';
|
||||||
part 'local_asset.model.dart';
|
part 'local_asset.model.dart';
|
||||||
|
|
||||||
enum AssetType {
|
enum AssetType {
|
||||||
|
|||||||
+18
-13
@@ -8,16 +8,18 @@ enum AssetVisibility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model for an asset stored in the server
|
// Model for an asset stored in the server
|
||||||
class Asset extends BaseAsset {
|
class RemoteAsset extends BaseAsset {
|
||||||
final String id;
|
final String id;
|
||||||
final String? localId;
|
final String? localId;
|
||||||
final String? thumbHash;
|
final String? thumbHash;
|
||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
|
final String ownerId;
|
||||||
|
|
||||||
const Asset({
|
const RemoteAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
this.localId,
|
this.localId,
|
||||||
required super.name,
|
required super.name,
|
||||||
|
required this.ownerId,
|
||||||
required super.checksum,
|
required super.checksum,
|
||||||
required super.type,
|
required super.type,
|
||||||
required super.createdAt,
|
required super.createdAt,
|
||||||
@@ -37,16 +39,17 @@ class Asset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
id: $id,
|
id: $id,
|
||||||
name: $name,
|
name: $name,
|
||||||
type: $type,
|
ownerId: $ownerId,
|
||||||
createdAt: $createdAt,
|
type: $type,
|
||||||
updatedAt: $updatedAt,
|
createdAt: $createdAt,
|
||||||
width: ${width ?? "<NA>"},
|
updatedAt: $updatedAt,
|
||||||
height: ${height ?? "<NA>"},
|
width: ${width ?? "<NA>"},
|
||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
height: ${height ?? "<NA>"},
|
||||||
localId: ${localId ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
isFavorite: $isFavorite,
|
localId: ${localId ?? "<NA>"},
|
||||||
|
isFavorite: $isFavorite,
|
||||||
thumbHash: ${thumbHash ?? "<NA>"},
|
thumbHash: ${thumbHash ?? "<NA>"},
|
||||||
visibility: $visibility,
|
visibility: $visibility,
|
||||||
}''';
|
}''';
|
||||||
@@ -54,10 +57,11 @@ class Asset extends BaseAsset {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! Asset) return false;
|
if (other is! RemoteAsset) return false;
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
return super == other &&
|
return super == other &&
|
||||||
id == other.id &&
|
id == other.id &&
|
||||||
|
ownerId == other.ownerId &&
|
||||||
localId == other.localId &&
|
localId == other.localId &&
|
||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility;
|
visibility == other.visibility;
|
||||||
@@ -67,6 +71,7 @@ class Asset extends BaseAsset {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
super.hashCode ^
|
super.hashCode ^
|
||||||
id.hashCode ^
|
id.hashCode ^
|
||||||
|
ownerId.hashCode ^
|
||||||
localId.hashCode ^
|
localId.hashCode ^
|
||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode;
|
visibility.hashCode;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/album/album.model.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/utils/remote_album.utils.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumService {
|
||||||
|
final DriftRemoteAlbumRepository _repository;
|
||||||
|
|
||||||
|
const RemoteAlbumService(this._repository);
|
||||||
|
|
||||||
|
Future<List<Album>> getAll() {
|
||||||
|
return _repository.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Album> sortAlbums(
|
||||||
|
List<Album> albums,
|
||||||
|
RemoteAlbumSortMode sortMode, {
|
||||||
|
bool isReverse = false,
|
||||||
|
}) {
|
||||||
|
return sortMode.sortFn(albums, isReverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Album> searchAlbums(
|
||||||
|
List<Album> albums,
|
||||||
|
String query,
|
||||||
|
String? userId, [
|
||||||
|
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||||
|
]) {
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
List<Album> filtered = albums;
|
||||||
|
|
||||||
|
// Apply text search filter
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
filtered = filtered
|
||||||
|
.where(
|
||||||
|
(album) =>
|
||||||
|
album.name.toLowerCase().contains(lowerQuery) ||
|
||||||
|
album.description.toLowerCase().contains(lowerQuery),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
switch (filterMode) {
|
||||||
|
case QuickFilterMode.myAlbums:
|
||||||
|
filtered =
|
||||||
|
filtered.where((album) => album.ownerId == userId).toList();
|
||||||
|
break;
|
||||||
|
case QuickFilterMode.sharedWithMe:
|
||||||
|
filtered =
|
||||||
|
filtered.where((album) => album.ownerId != userId).toList();
|
||||||
|
break;
|
||||||
|
case QuickFilterMode.all:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ class TimelineService {
|
|||||||
}) : _assetSource = assetSource,
|
}) : _assetSource = assetSource,
|
||||||
_bucketSource = bucketSource {
|
_bucketSource = bucketSource {
|
||||||
_bucketSubscription =
|
_bucketSubscription =
|
||||||
_bucketSource().listen((_) => unawaited(_reloadBucket()));
|
_bucketSource().listen((_) => unawaited(reloadBucket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final AsyncMutex _mutex = AsyncMutex();
|
final AsyncMutex _mutex = AsyncMutex();
|
||||||
@@ -74,7 +74,7 @@ class TimelineService {
|
|||||||
|
|
||||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||||
|
|
||||||
Future<void> _reloadBucket() => _mutex.run(() async {
|
Future<void> reloadBucket() => _mutex.run(() async {
|
||||||
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -116,15 +116,15 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
|||||||
|
|
||||||
TextColumn get exposureTime => text().nullable()();
|
TextColumn get exposureTime => text().nullable()();
|
||||||
|
|
||||||
IntColumn get fNumber => integer().nullable()();
|
RealColumn get fNumber => real().nullable()();
|
||||||
|
|
||||||
IntColumn get fileSize => integer().nullable()();
|
IntColumn get fileSize => integer().nullable()();
|
||||||
|
|
||||||
IntColumn get focalLength => integer().nullable()();
|
RealColumn get focalLength => real().nullable()();
|
||||||
|
|
||||||
IntColumn get latitude => integer().nullable()();
|
RealColumn get latitude => real().nullable()();
|
||||||
|
|
||||||
IntColumn get longitude => integer().nullable()();
|
RealColumn get longitude => real().nullable()();
|
||||||
|
|
||||||
IntColumn get iso => integer().nullable()();
|
IntColumn get iso => integer().nullable()();
|
||||||
|
|
||||||
|
|||||||
+77
-77
@@ -19,11 +19,11 @@ typedef $$RemoteExifEntityTableCreateCompanionBuilder
|
|||||||
i0.Value<int?> height,
|
i0.Value<int?> height,
|
||||||
i0.Value<int?> width,
|
i0.Value<int?> width,
|
||||||
i0.Value<String?> exposureTime,
|
i0.Value<String?> exposureTime,
|
||||||
i0.Value<int?> fNumber,
|
i0.Value<double?> fNumber,
|
||||||
i0.Value<int?> fileSize,
|
i0.Value<int?> fileSize,
|
||||||
i0.Value<int?> focalLength,
|
i0.Value<double?> focalLength,
|
||||||
i0.Value<int?> latitude,
|
i0.Value<double?> latitude,
|
||||||
i0.Value<int?> longitude,
|
i0.Value<double?> longitude,
|
||||||
i0.Value<int?> iso,
|
i0.Value<int?> iso,
|
||||||
i0.Value<String?> make,
|
i0.Value<String?> make,
|
||||||
i0.Value<String?> model,
|
i0.Value<String?> model,
|
||||||
@@ -43,11 +43,11 @@ typedef $$RemoteExifEntityTableUpdateCompanionBuilder
|
|||||||
i0.Value<int?> height,
|
i0.Value<int?> height,
|
||||||
i0.Value<int?> width,
|
i0.Value<int?> width,
|
||||||
i0.Value<String?> exposureTime,
|
i0.Value<String?> exposureTime,
|
||||||
i0.Value<int?> fNumber,
|
i0.Value<double?> fNumber,
|
||||||
i0.Value<int?> fileSize,
|
i0.Value<int?> fileSize,
|
||||||
i0.Value<int?> focalLength,
|
i0.Value<double?> focalLength,
|
||||||
i0.Value<int?> latitude,
|
i0.Value<double?> latitude,
|
||||||
i0.Value<int?> longitude,
|
i0.Value<double?> longitude,
|
||||||
i0.Value<int?> iso,
|
i0.Value<int?> iso,
|
||||||
i0.Value<String?> make,
|
i0.Value<String?> make,
|
||||||
i0.Value<String?> model,
|
i0.Value<String?> model,
|
||||||
@@ -125,20 +125,20 @@ class $$RemoteExifEntityTableFilterComposer
|
|||||||
column: $table.exposureTime,
|
column: $table.exposureTime,
|
||||||
builder: (column) => i0.ColumnFilters(column));
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<int> get fNumber => $composableBuilder(
|
i0.ColumnFilters<double> get fNumber => $composableBuilder(
|
||||||
column: $table.fNumber, builder: (column) => i0.ColumnFilters(column));
|
column: $table.fNumber, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<int> get fileSize => $composableBuilder(
|
i0.ColumnFilters<int> get fileSize => $composableBuilder(
|
||||||
column: $table.fileSize, builder: (column) => i0.ColumnFilters(column));
|
column: $table.fileSize, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<int> get focalLength => $composableBuilder(
|
i0.ColumnFilters<double> get focalLength => $composableBuilder(
|
||||||
column: $table.focalLength,
|
column: $table.focalLength,
|
||||||
builder: (column) => i0.ColumnFilters(column));
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<int> get latitude => $composableBuilder(
|
i0.ColumnFilters<double> get latitude => $composableBuilder(
|
||||||
column: $table.latitude, builder: (column) => i0.ColumnFilters(column));
|
column: $table.latitude, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<int> get longitude => $composableBuilder(
|
i0.ColumnFilters<double> get longitude => $composableBuilder(
|
||||||
column: $table.longitude, builder: (column) => i0.ColumnFilters(column));
|
column: $table.longitude, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<int> get iso => $composableBuilder(
|
i0.ColumnFilters<int> get iso => $composableBuilder(
|
||||||
@@ -223,20 +223,20 @@ class $$RemoteExifEntityTableOrderingComposer
|
|||||||
column: $table.exposureTime,
|
column: $table.exposureTime,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get fNumber => $composableBuilder(
|
i0.ColumnOrderings<double> get fNumber => $composableBuilder(
|
||||||
column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column));
|
column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get fileSize => $composableBuilder(
|
i0.ColumnOrderings<int> get fileSize => $composableBuilder(
|
||||||
column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column));
|
column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get focalLength => $composableBuilder(
|
i0.ColumnOrderings<double> get focalLength => $composableBuilder(
|
||||||
column: $table.focalLength,
|
column: $table.focalLength,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get latitude => $composableBuilder(
|
i0.ColumnOrderings<double> get latitude => $composableBuilder(
|
||||||
column: $table.latitude, builder: (column) => i0.ColumnOrderings(column));
|
column: $table.latitude, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i0.ColumnOrderings<int> get longitude => $composableBuilder(
|
i0.ColumnOrderings<double> get longitude => $composableBuilder(
|
||||||
column: $table.longitude,
|
column: $table.longitude,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
@@ -321,19 +321,19 @@ class $$RemoteExifEntityTableAnnotationComposer
|
|||||||
i0.GeneratedColumn<String> get exposureTime => $composableBuilder(
|
i0.GeneratedColumn<String> get exposureTime => $composableBuilder(
|
||||||
column: $table.exposureTime, builder: (column) => column);
|
column: $table.exposureTime, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get fNumber =>
|
i0.GeneratedColumn<double> get fNumber =>
|
||||||
$composableBuilder(column: $table.fNumber, builder: (column) => column);
|
$composableBuilder(column: $table.fNumber, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get fileSize =>
|
i0.GeneratedColumn<int> get fileSize =>
|
||||||
$composableBuilder(column: $table.fileSize, builder: (column) => column);
|
$composableBuilder(column: $table.fileSize, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get focalLength => $composableBuilder(
|
i0.GeneratedColumn<double> get focalLength => $composableBuilder(
|
||||||
column: $table.focalLength, builder: (column) => column);
|
column: $table.focalLength, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get latitude =>
|
i0.GeneratedColumn<double> get latitude =>
|
||||||
$composableBuilder(column: $table.latitude, builder: (column) => column);
|
$composableBuilder(column: $table.latitude, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get longitude =>
|
i0.GeneratedColumn<double> get longitude =>
|
||||||
$composableBuilder(column: $table.longitude, builder: (column) => column);
|
$composableBuilder(column: $table.longitude, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<int> get iso =>
|
i0.GeneratedColumn<int> get iso =>
|
||||||
@@ -416,11 +416,11 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<int?> height = const i0.Value.absent(),
|
i0.Value<int?> height = const i0.Value.absent(),
|
||||||
i0.Value<int?> width = const i0.Value.absent(),
|
i0.Value<int?> width = const i0.Value.absent(),
|
||||||
i0.Value<String?> exposureTime = const i0.Value.absent(),
|
i0.Value<String?> exposureTime = const i0.Value.absent(),
|
||||||
i0.Value<int?> fNumber = const i0.Value.absent(),
|
i0.Value<double?> fNumber = const i0.Value.absent(),
|
||||||
i0.Value<int?> fileSize = const i0.Value.absent(),
|
i0.Value<int?> fileSize = const i0.Value.absent(),
|
||||||
i0.Value<int?> focalLength = const i0.Value.absent(),
|
i0.Value<double?> focalLength = const i0.Value.absent(),
|
||||||
i0.Value<int?> latitude = const i0.Value.absent(),
|
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||||
i0.Value<int?> longitude = const i0.Value.absent(),
|
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||||
i0.Value<int?> iso = const i0.Value.absent(),
|
i0.Value<int?> iso = const i0.Value.absent(),
|
||||||
i0.Value<String?> make = const i0.Value.absent(),
|
i0.Value<String?> make = const i0.Value.absent(),
|
||||||
i0.Value<String?> model = const i0.Value.absent(),
|
i0.Value<String?> model = const i0.Value.absent(),
|
||||||
@@ -462,11 +462,11 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<int?> height = const i0.Value.absent(),
|
i0.Value<int?> height = const i0.Value.absent(),
|
||||||
i0.Value<int?> width = const i0.Value.absent(),
|
i0.Value<int?> width = const i0.Value.absent(),
|
||||||
i0.Value<String?> exposureTime = const i0.Value.absent(),
|
i0.Value<String?> exposureTime = const i0.Value.absent(),
|
||||||
i0.Value<int?> fNumber = const i0.Value.absent(),
|
i0.Value<double?> fNumber = const i0.Value.absent(),
|
||||||
i0.Value<int?> fileSize = const i0.Value.absent(),
|
i0.Value<int?> fileSize = const i0.Value.absent(),
|
||||||
i0.Value<int?> focalLength = const i0.Value.absent(),
|
i0.Value<double?> focalLength = const i0.Value.absent(),
|
||||||
i0.Value<int?> latitude = const i0.Value.absent(),
|
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||||
i0.Value<int?> longitude = const i0.Value.absent(),
|
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||||
i0.Value<int?> iso = const i0.Value.absent(),
|
i0.Value<int?> iso = const i0.Value.absent(),
|
||||||
i0.Value<String?> make = const i0.Value.absent(),
|
i0.Value<String?> make = const i0.Value.absent(),
|
||||||
i0.Value<String?> model = const i0.Value.absent(),
|
i0.Value<String?> model = const i0.Value.absent(),
|
||||||
@@ -622,9 +622,9 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
static const i0.VerificationMeta _fNumberMeta =
|
static const i0.VerificationMeta _fNumberMeta =
|
||||||
const i0.VerificationMeta('fNumber');
|
const i0.VerificationMeta('fNumber');
|
||||||
@override
|
@override
|
||||||
late final i0.GeneratedColumn<int> fNumber = i0.GeneratedColumn<int>(
|
late final i0.GeneratedColumn<double> fNumber = i0.GeneratedColumn<double>(
|
||||||
'f_number', aliasedName, true,
|
'f_number', aliasedName, true,
|
||||||
type: i0.DriftSqlType.int, requiredDuringInsert: false);
|
type: i0.DriftSqlType.double, requiredDuringInsert: false);
|
||||||
static const i0.VerificationMeta _fileSizeMeta =
|
static const i0.VerificationMeta _fileSizeMeta =
|
||||||
const i0.VerificationMeta('fileSize');
|
const i0.VerificationMeta('fileSize');
|
||||||
@override
|
@override
|
||||||
@@ -634,21 +634,21 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
static const i0.VerificationMeta _focalLengthMeta =
|
static const i0.VerificationMeta _focalLengthMeta =
|
||||||
const i0.VerificationMeta('focalLength');
|
const i0.VerificationMeta('focalLength');
|
||||||
@override
|
@override
|
||||||
late final i0.GeneratedColumn<int> focalLength = i0.GeneratedColumn<int>(
|
late final i0.GeneratedColumn<double> focalLength =
|
||||||
'focal_length', aliasedName, true,
|
i0.GeneratedColumn<double>('focal_length', aliasedName, true,
|
||||||
type: i0.DriftSqlType.int, requiredDuringInsert: false);
|
type: i0.DriftSqlType.double, requiredDuringInsert: false);
|
||||||
static const i0.VerificationMeta _latitudeMeta =
|
static const i0.VerificationMeta _latitudeMeta =
|
||||||
const i0.VerificationMeta('latitude');
|
const i0.VerificationMeta('latitude');
|
||||||
@override
|
@override
|
||||||
late final i0.GeneratedColumn<int> latitude = i0.GeneratedColumn<int>(
|
late final i0.GeneratedColumn<double> latitude = i0.GeneratedColumn<double>(
|
||||||
'latitude', aliasedName, true,
|
'latitude', aliasedName, true,
|
||||||
type: i0.DriftSqlType.int, requiredDuringInsert: false);
|
type: i0.DriftSqlType.double, requiredDuringInsert: false);
|
||||||
static const i0.VerificationMeta _longitudeMeta =
|
static const i0.VerificationMeta _longitudeMeta =
|
||||||
const i0.VerificationMeta('longitude');
|
const i0.VerificationMeta('longitude');
|
||||||
@override
|
@override
|
||||||
late final i0.GeneratedColumn<int> longitude = i0.GeneratedColumn<int>(
|
late final i0.GeneratedColumn<double> longitude = i0.GeneratedColumn<double>(
|
||||||
'longitude', aliasedName, true,
|
'longitude', aliasedName, true,
|
||||||
type: i0.DriftSqlType.int, requiredDuringInsert: false);
|
type: i0.DriftSqlType.double, requiredDuringInsert: false);
|
||||||
static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso');
|
static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso');
|
||||||
@override
|
@override
|
||||||
late final i0.GeneratedColumn<int> iso = i0.GeneratedColumn<int>(
|
late final i0.GeneratedColumn<int> iso = i0.GeneratedColumn<int>(
|
||||||
@@ -853,15 +853,15 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
exposureTime: attachedDatabase.typeMapping.read(
|
exposureTime: attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']),
|
i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']),
|
||||||
fNumber: attachedDatabase.typeMapping
|
fNumber: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}f_number']),
|
.read(i0.DriftSqlType.double, data['${effectivePrefix}f_number']),
|
||||||
fileSize: attachedDatabase.typeMapping
|
fileSize: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']),
|
.read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']),
|
||||||
focalLength: attachedDatabase.typeMapping
|
focalLength: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}focal_length']),
|
.read(i0.DriftSqlType.double, data['${effectivePrefix}focal_length']),
|
||||||
latitude: attachedDatabase.typeMapping
|
latitude: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}latitude']),
|
.read(i0.DriftSqlType.double, data['${effectivePrefix}latitude']),
|
||||||
longitude: attachedDatabase.typeMapping
|
longitude: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}longitude']),
|
.read(i0.DriftSqlType.double, data['${effectivePrefix}longitude']),
|
||||||
iso: attachedDatabase.typeMapping
|
iso: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}iso']),
|
.read(i0.DriftSqlType.int, data['${effectivePrefix}iso']),
|
||||||
make: attachedDatabase.typeMapping
|
make: attachedDatabase.typeMapping
|
||||||
@@ -901,11 +901,11 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
final int? height;
|
final int? height;
|
||||||
final int? width;
|
final int? width;
|
||||||
final String? exposureTime;
|
final String? exposureTime;
|
||||||
final int? fNumber;
|
final double? fNumber;
|
||||||
final int? fileSize;
|
final int? fileSize;
|
||||||
final int? focalLength;
|
final double? focalLength;
|
||||||
final int? latitude;
|
final double? latitude;
|
||||||
final int? longitude;
|
final double? longitude;
|
||||||
final int? iso;
|
final int? iso;
|
||||||
final String? make;
|
final String? make;
|
||||||
final String? model;
|
final String? model;
|
||||||
@@ -964,19 +964,19 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
map['exposure_time'] = i0.Variable<String>(exposureTime);
|
map['exposure_time'] = i0.Variable<String>(exposureTime);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || fNumber != null) {
|
if (!nullToAbsent || fNumber != null) {
|
||||||
map['f_number'] = i0.Variable<int>(fNumber);
|
map['f_number'] = i0.Variable<double>(fNumber);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || fileSize != null) {
|
if (!nullToAbsent || fileSize != null) {
|
||||||
map['file_size'] = i0.Variable<int>(fileSize);
|
map['file_size'] = i0.Variable<int>(fileSize);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || focalLength != null) {
|
if (!nullToAbsent || focalLength != null) {
|
||||||
map['focal_length'] = i0.Variable<int>(focalLength);
|
map['focal_length'] = i0.Variable<double>(focalLength);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || latitude != null) {
|
if (!nullToAbsent || latitude != null) {
|
||||||
map['latitude'] = i0.Variable<int>(latitude);
|
map['latitude'] = i0.Variable<double>(latitude);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || longitude != null) {
|
if (!nullToAbsent || longitude != null) {
|
||||||
map['longitude'] = i0.Variable<int>(longitude);
|
map['longitude'] = i0.Variable<double>(longitude);
|
||||||
}
|
}
|
||||||
if (!nullToAbsent || iso != null) {
|
if (!nullToAbsent || iso != null) {
|
||||||
map['iso'] = i0.Variable<int>(iso);
|
map['iso'] = i0.Variable<int>(iso);
|
||||||
@@ -1016,11 +1016,11 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
height: serializer.fromJson<int?>(json['height']),
|
height: serializer.fromJson<int?>(json['height']),
|
||||||
width: serializer.fromJson<int?>(json['width']),
|
width: serializer.fromJson<int?>(json['width']),
|
||||||
exposureTime: serializer.fromJson<String?>(json['exposureTime']),
|
exposureTime: serializer.fromJson<String?>(json['exposureTime']),
|
||||||
fNumber: serializer.fromJson<int?>(json['fNumber']),
|
fNumber: serializer.fromJson<double?>(json['fNumber']),
|
||||||
fileSize: serializer.fromJson<int?>(json['fileSize']),
|
fileSize: serializer.fromJson<int?>(json['fileSize']),
|
||||||
focalLength: serializer.fromJson<int?>(json['focalLength']),
|
focalLength: serializer.fromJson<double?>(json['focalLength']),
|
||||||
latitude: serializer.fromJson<int?>(json['latitude']),
|
latitude: serializer.fromJson<double?>(json['latitude']),
|
||||||
longitude: serializer.fromJson<int?>(json['longitude']),
|
longitude: serializer.fromJson<double?>(json['longitude']),
|
||||||
iso: serializer.fromJson<int?>(json['iso']),
|
iso: serializer.fromJson<int?>(json['iso']),
|
||||||
make: serializer.fromJson<String?>(json['make']),
|
make: serializer.fromJson<String?>(json['make']),
|
||||||
model: serializer.fromJson<String?>(json['model']),
|
model: serializer.fromJson<String?>(json['model']),
|
||||||
@@ -1043,11 +1043,11 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
'height': serializer.toJson<int?>(height),
|
'height': serializer.toJson<int?>(height),
|
||||||
'width': serializer.toJson<int?>(width),
|
'width': serializer.toJson<int?>(width),
|
||||||
'exposureTime': serializer.toJson<String?>(exposureTime),
|
'exposureTime': serializer.toJson<String?>(exposureTime),
|
||||||
'fNumber': serializer.toJson<int?>(fNumber),
|
'fNumber': serializer.toJson<double?>(fNumber),
|
||||||
'fileSize': serializer.toJson<int?>(fileSize),
|
'fileSize': serializer.toJson<int?>(fileSize),
|
||||||
'focalLength': serializer.toJson<int?>(focalLength),
|
'focalLength': serializer.toJson<double?>(focalLength),
|
||||||
'latitude': serializer.toJson<int?>(latitude),
|
'latitude': serializer.toJson<double?>(latitude),
|
||||||
'longitude': serializer.toJson<int?>(longitude),
|
'longitude': serializer.toJson<double?>(longitude),
|
||||||
'iso': serializer.toJson<int?>(iso),
|
'iso': serializer.toJson<int?>(iso),
|
||||||
'make': serializer.toJson<String?>(make),
|
'make': serializer.toJson<String?>(make),
|
||||||
'model': serializer.toJson<String?>(model),
|
'model': serializer.toJson<String?>(model),
|
||||||
@@ -1068,11 +1068,11 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
i0.Value<int?> height = const i0.Value.absent(),
|
i0.Value<int?> height = const i0.Value.absent(),
|
||||||
i0.Value<int?> width = const i0.Value.absent(),
|
i0.Value<int?> width = const i0.Value.absent(),
|
||||||
i0.Value<String?> exposureTime = const i0.Value.absent(),
|
i0.Value<String?> exposureTime = const i0.Value.absent(),
|
||||||
i0.Value<int?> fNumber = const i0.Value.absent(),
|
i0.Value<double?> fNumber = const i0.Value.absent(),
|
||||||
i0.Value<int?> fileSize = const i0.Value.absent(),
|
i0.Value<int?> fileSize = const i0.Value.absent(),
|
||||||
i0.Value<int?> focalLength = const i0.Value.absent(),
|
i0.Value<double?> focalLength = const i0.Value.absent(),
|
||||||
i0.Value<int?> latitude = const i0.Value.absent(),
|
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||||
i0.Value<int?> longitude = const i0.Value.absent(),
|
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||||
i0.Value<int?> iso = const i0.Value.absent(),
|
i0.Value<int?> iso = const i0.Value.absent(),
|
||||||
i0.Value<String?> make = const i0.Value.absent(),
|
i0.Value<String?> make = const i0.Value.absent(),
|
||||||
i0.Value<String?> model = const i0.Value.absent(),
|
i0.Value<String?> model = const i0.Value.absent(),
|
||||||
@@ -1232,11 +1232,11 @@ class RemoteExifEntityCompanion
|
|||||||
final i0.Value<int?> height;
|
final i0.Value<int?> height;
|
||||||
final i0.Value<int?> width;
|
final i0.Value<int?> width;
|
||||||
final i0.Value<String?> exposureTime;
|
final i0.Value<String?> exposureTime;
|
||||||
final i0.Value<int?> fNumber;
|
final i0.Value<double?> fNumber;
|
||||||
final i0.Value<int?> fileSize;
|
final i0.Value<int?> fileSize;
|
||||||
final i0.Value<int?> focalLength;
|
final i0.Value<double?> focalLength;
|
||||||
final i0.Value<int?> latitude;
|
final i0.Value<double?> latitude;
|
||||||
final i0.Value<int?> longitude;
|
final i0.Value<double?> longitude;
|
||||||
final i0.Value<int?> iso;
|
final i0.Value<int?> iso;
|
||||||
final i0.Value<String?> make;
|
final i0.Value<String?> make;
|
||||||
final i0.Value<String?> model;
|
final i0.Value<String?> model;
|
||||||
@@ -1300,11 +1300,11 @@ class RemoteExifEntityCompanion
|
|||||||
i0.Expression<int>? height,
|
i0.Expression<int>? height,
|
||||||
i0.Expression<int>? width,
|
i0.Expression<int>? width,
|
||||||
i0.Expression<String>? exposureTime,
|
i0.Expression<String>? exposureTime,
|
||||||
i0.Expression<int>? fNumber,
|
i0.Expression<double>? fNumber,
|
||||||
i0.Expression<int>? fileSize,
|
i0.Expression<int>? fileSize,
|
||||||
i0.Expression<int>? focalLength,
|
i0.Expression<double>? focalLength,
|
||||||
i0.Expression<int>? latitude,
|
i0.Expression<double>? latitude,
|
||||||
i0.Expression<int>? longitude,
|
i0.Expression<double>? longitude,
|
||||||
i0.Expression<int>? iso,
|
i0.Expression<int>? iso,
|
||||||
i0.Expression<String>? make,
|
i0.Expression<String>? make,
|
||||||
i0.Expression<String>? model,
|
i0.Expression<String>? model,
|
||||||
@@ -1348,11 +1348,11 @@ class RemoteExifEntityCompanion
|
|||||||
i0.Value<int?>? height,
|
i0.Value<int?>? height,
|
||||||
i0.Value<int?>? width,
|
i0.Value<int?>? width,
|
||||||
i0.Value<String?>? exposureTime,
|
i0.Value<String?>? exposureTime,
|
||||||
i0.Value<int?>? fNumber,
|
i0.Value<double?>? fNumber,
|
||||||
i0.Value<int?>? fileSize,
|
i0.Value<int?>? fileSize,
|
||||||
i0.Value<int?>? focalLength,
|
i0.Value<double?>? focalLength,
|
||||||
i0.Value<int?>? latitude,
|
i0.Value<double?>? latitude,
|
||||||
i0.Value<int?>? longitude,
|
i0.Value<double?>? longitude,
|
||||||
i0.Value<int?>? iso,
|
i0.Value<int?>? iso,
|
||||||
i0.Value<String?>? make,
|
i0.Value<String?>? make,
|
||||||
i0.Value<String?>? model,
|
i0.Value<String?>? model,
|
||||||
@@ -1416,19 +1416,19 @@ class RemoteExifEntityCompanion
|
|||||||
map['exposure_time'] = i0.Variable<String>(exposureTime.value);
|
map['exposure_time'] = i0.Variable<String>(exposureTime.value);
|
||||||
}
|
}
|
||||||
if (fNumber.present) {
|
if (fNumber.present) {
|
||||||
map['f_number'] = i0.Variable<int>(fNumber.value);
|
map['f_number'] = i0.Variable<double>(fNumber.value);
|
||||||
}
|
}
|
||||||
if (fileSize.present) {
|
if (fileSize.present) {
|
||||||
map['file_size'] = i0.Variable<int>(fileSize.value);
|
map['file_size'] = i0.Variable<int>(fileSize.value);
|
||||||
}
|
}
|
||||||
if (focalLength.present) {
|
if (focalLength.present) {
|
||||||
map['focal_length'] = i0.Variable<int>(focalLength.value);
|
map['focal_length'] = i0.Variable<double>(focalLength.value);
|
||||||
}
|
}
|
||||||
if (latitude.present) {
|
if (latitude.present) {
|
||||||
map['latitude'] = i0.Variable<int>(latitude.value);
|
map['latitude'] = i0.Variable<double>(latitude.value);
|
||||||
}
|
}
|
||||||
if (longitude.present) {
|
if (longitude.present) {
|
||||||
map['longitude'] = i0.Variable<int>(longitude.value);
|
map['longitude'] = i0.Variable<double>(longitude.value);
|
||||||
}
|
}
|
||||||
if (iso.present) {
|
if (iso.present) {
|
||||||
map['iso'] = i0.Variable<int>(iso.value);
|
map['iso'] = i0.Variable<int>(iso.value);
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ class RemoteAssetEntity extends Table
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||||
Asset toDto() => Asset(
|
RemoteAsset toDto() => RemoteAsset(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
|
ownerId: ownerId,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
type: type,
|
type: type,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||||
as entity;
|
as entity;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
@@ -41,3 +43,36 @@ class IsarExifRepository extends IsarDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DriftRemoteExifRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftRemoteExifRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<ExifInfo?> get(String assetId) {
|
||||||
|
final query = _db.remoteExifEntity.select()
|
||||||
|
..where((exif) => exif.assetId.equals(assetId));
|
||||||
|
|
||||||
|
return query.map((asset) => asset.toDto()).getSingleOrNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on RemoteExifEntityData {
|
||||||
|
ExifInfo toDto() {
|
||||||
|
return ExifInfo(
|
||||||
|
fileSize: fileSize,
|
||||||
|
description: description,
|
||||||
|
orientation: orientation,
|
||||||
|
timeZone: timeZone,
|
||||||
|
dateTimeOriginal: dateTimeOriginal,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
city: city,
|
||||||
|
state: state,
|
||||||
|
country: country,
|
||||||
|
make: make,
|
||||||
|
model: model,
|
||||||
|
f: fNumber,
|
||||||
|
iso: iso,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,26 +10,48 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
||||||
|
|
||||||
Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
||||||
final query = _db.remoteAlbumEntity.select();
|
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
||||||
|
|
||||||
|
final query = _db.remoteAlbumEntity.select().join([
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
query
|
||||||
|
..addColumns([assetCount])
|
||||||
|
..groupBy([_db.remoteAlbumEntity.id]);
|
||||||
|
|
||||||
if (sortBy.isNotEmpty) {
|
if (sortBy.isNotEmpty) {
|
||||||
final orderings = <OrderClauseGenerator<$RemoteAlbumEntityTable>>[];
|
final orderings = <OrderingTerm>[];
|
||||||
for (final sort in sortBy) {
|
for (final sort in sortBy) {
|
||||||
orderings.add(
|
orderings.add(
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
SortRemoteAlbumsBy.id => (row) => OrderingTerm.asc(row.id),
|
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
query.orderBy(orderings);
|
query.orderBy(orderings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
return query
|
||||||
|
.map(
|
||||||
|
(row) => row.readTable(_db.remoteAlbumEntity).toDto(
|
||||||
|
assetCount: row.read(assetCount) ?? 0,
|
||||||
|
ownerName: row.readTable(_db.userEntity).name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
Album toDto() {
|
Album toDto({int assetCount = 0, required String ownerName}) {
|
||||||
return Album(
|
return Album(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -40,6 +62,8 @@ extension on RemoteAlbumEntityData {
|
|||||||
thumbnailAssetId: thumbnailAssetId,
|
thumbnailAssetId: thumbnailAssetId,
|
||||||
isActivityEnabled: isActivityEnabled,
|
isActivityEnabled: isActivityEnabled,
|
||||||
order: order,
|
order: order,
|
||||||
|
assetCount: assetCount,
|
||||||
|
ownerName: ownerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
class DriftRemoteAssetRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftRemoteAssetRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(isFavorite: Value(isFavorite)),
|
||||||
|
where: (e) => e.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(visibility: Value(visibility)),
|
||||||
|
where: (e) => e.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteExifEntity,
|
||||||
|
RemoteExifEntityCompanion(
|
||||||
|
latitude: Value(location.latitude),
|
||||||
|
longitude: Value(location.longitude),
|
||||||
|
),
|
||||||
|
where: (e) => e.assetId.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,36 +70,38 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return _db.mergedAssetDrift
|
return _db.mergedAssetDrift
|
||||||
.mergedAsset(userIds, limit: Limit(count, offset))
|
.mergedAsset(userIds, limit: Limit(count, offset))
|
||||||
.map(
|
.map(
|
||||||
(row) => row.remoteId != null
|
(row) {
|
||||||
? Asset(
|
return row.remoteId != null && row.ownerId != null
|
||||||
id: row.remoteId!,
|
? RemoteAsset(
|
||||||
localId: row.localId,
|
id: row.remoteId!,
|
||||||
name: row.name,
|
localId: row.localId,
|
||||||
checksum: row.checksum,
|
name: row.name,
|
||||||
type: row.type,
|
ownerId: row.ownerId!,
|
||||||
createdAt: row.createdAt,
|
checksum: row.checksum,
|
||||||
updatedAt: row.updatedAt,
|
type: row.type,
|
||||||
thumbHash: row.thumbHash,
|
createdAt: row.createdAt,
|
||||||
width: row.width,
|
updatedAt: row.updatedAt,
|
||||||
height: row.height,
|
thumbHash: row.thumbHash,
|
||||||
isFavorite: row.isFavorite,
|
width: row.width,
|
||||||
durationInSeconds: row.durationInSeconds,
|
height: row.height,
|
||||||
)
|
isFavorite: row.isFavorite,
|
||||||
: LocalAsset(
|
durationInSeconds: row.durationInSeconds,
|
||||||
id: row.localId!,
|
)
|
||||||
remoteId: row.remoteId,
|
: LocalAsset(
|
||||||
name: row.name,
|
id: row.localId!,
|
||||||
checksum: row.checksum,
|
remoteId: row.remoteId,
|
||||||
type: row.type,
|
name: row.name,
|
||||||
createdAt: row.createdAt,
|
checksum: row.checksum,
|
||||||
updatedAt: row.updatedAt,
|
type: row.type,
|
||||||
width: row.width,
|
createdAt: row.createdAt,
|
||||||
height: row.height,
|
updatedAt: row.updatedAt,
|
||||||
isFavorite: row.isFavorite,
|
width: row.width,
|
||||||
durationInSeconds: row.durationInSeconds,
|
height: row.height,
|
||||||
),
|
isFavorite: row.isFavorite,
|
||||||
)
|
durationInSeconds: row.durationInSeconds,
|
||||||
.get();
|
);
|
||||||
|
},
|
||||||
|
).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchLocalBucket(
|
Stream<List<Bucket>> watchLocalBucket(
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class TabShellPage extends ConsumerWidget {
|
class TabShellPage extends ConsumerWidget {
|
||||||
@@ -138,12 +138,13 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
final multiselectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: [
|
||||||
const MainTimelineRoute(),
|
const MainTimelineRoute(),
|
||||||
SearchRoute(),
|
SearchRoute(),
|
||||||
const AlbumsRoute(),
|
const DriftAlbumsRoute(),
|
||||||
const LibraryRoute(),
|
const LibraryRoute(),
|
||||||
],
|
],
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 600),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -20,7 +18,7 @@ class LocalTimelinePage extends StatelessWidget {
|
|||||||
(ref) {
|
(ref) {
|
||||||
final timelineService =
|
final timelineService =
|
||||||
ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId);
|
ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId);
|
||||||
ref.onDispose(() => unawaited(timelineService.dispose()));
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
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/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MainTimelinePage extends StatelessWidget {
|
class MainTimelinePage extends ConsumerWidget {
|
||||||
const MainTimelinePage({super.key});
|
const MainTimelinePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ProviderScope(
|
return const Timeline();
|
||||||
overrides: [
|
|
||||||
timelineServiceProvider.overrideWith(
|
|
||||||
(ref) {
|
|
||||||
final timelineUsers =
|
|
||||||
ref.watch(timelineUsersProvider).valueOrNull ?? [];
|
|
||||||
final timelineService =
|
|
||||||
ref.watch(timelineFactoryProvider).main(timelineUsers);
|
|
||||||
ref.onDispose(() => unawaited(timelineService.dispose()));
|
|
||||||
return timelineService;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: const Timeline(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -21,7 +19,7 @@ class RemoteTimelinePage extends StatelessWidget {
|
|||||||
final timelineService = ref
|
final timelineService = ref
|
||||||
.watch(timelineFactoryProvider)
|
.watch(timelineFactoryProvider)
|
||||||
.remoteAlbum(albumId: albumId);
|
.remoteAlbum(albumId: albumId);
|
||||||
ref.onDispose(() => unawaited(timelineService.dispose()));
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,767 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftAlbumsPage extends ConsumerStatefulWidget {
|
||||||
|
const DriftAlbumsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftAlbumsPage> createState() => _DriftAlbumsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||||
|
bool isGrid = false;
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||||
|
final searchFocusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// Load albums when component mounts
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref.read(remoteAlbumProvider.notifier).getAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
searchController.addListener(() {
|
||||||
|
onSearch(searchController.text, filterMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||||
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
ref
|
||||||
|
.read(remoteAlbumProvider.notifier)
|
||||||
|
.searchAlbums(searchTerm, userId, sortMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onRefresh() async {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleViewMode() {
|
||||||
|
setState(() {
|
||||||
|
isGrid = !isGrid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeFilter(QuickFilterMode sortMode) {
|
||||||
|
setState(() {
|
||||||
|
filterMode = sortMode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSearch() {
|
||||||
|
setState(() {
|
||||||
|
filterMode = QuickFilterMode.all;
|
||||||
|
searchController.clear();
|
||||||
|
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
searchController.dispose();
|
||||||
|
searchFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final albumState = ref.watch(remoteAlbumProvider);
|
||||||
|
final albums = albumState.filteredAlbums;
|
||||||
|
final isLoading = albumState.isLoading;
|
||||||
|
final error = albumState.error;
|
||||||
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: onRefresh,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
const ImmichSliverAppBar(),
|
||||||
|
_SearchBar(
|
||||||
|
searchController: searchController,
|
||||||
|
searchFocusNode: searchFocusNode,
|
||||||
|
onSearch: onSearch,
|
||||||
|
filterMode: filterMode,
|
||||||
|
onClearSearch: clearSearch,
|
||||||
|
),
|
||||||
|
_QuickFilterButtonRow(
|
||||||
|
filterMode: filterMode,
|
||||||
|
onChangeFilter: changeFilter,
|
||||||
|
onSearch: onSearch,
|
||||||
|
searchController: searchController,
|
||||||
|
),
|
||||||
|
_QuickSortAndViewMode(
|
||||||
|
isGrid: isGrid,
|
||||||
|
onToggleViewMode: toggleViewMode,
|
||||||
|
),
|
||||||
|
isGrid
|
||||||
|
? _AlbumGrid(
|
||||||
|
albums: albums,
|
||||||
|
userId: userId,
|
||||||
|
isLoading: isLoading,
|
||||||
|
error: error,
|
||||||
|
)
|
||||||
|
: _AlbumList(
|
||||||
|
albums: albums,
|
||||||
|
userId: userId,
|
||||||
|
isLoading: isLoading,
|
||||||
|
error: error,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SortButton extends ConsumerStatefulWidget {
|
||||||
|
const _SortButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SortButtonState extends ConsumerState<_SortButton> {
|
||||||
|
RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified;
|
||||||
|
bool albumSortIsReverse = true;
|
||||||
|
|
||||||
|
void onMenuTapped(RemoteAlbumSortMode sortMode) {
|
||||||
|
final selected = albumSortOption == sortMode;
|
||||||
|
// Switch direction
|
||||||
|
if (selected) {
|
||||||
|
setState(() {
|
||||||
|
albumSortIsReverse = !albumSortIsReverse;
|
||||||
|
});
|
||||||
|
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
|
||||||
|
sortMode,
|
||||||
|
isReverse: albumSortIsReverse,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
albumSortOption = sortMode;
|
||||||
|
});
|
||||||
|
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
|
||||||
|
sortMode,
|
||||||
|
isReverse: albumSortIsReverse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MenuAnchor(
|
||||||
|
style: MenuStyle(
|
||||||
|
elevation: const WidgetStatePropertyAll(1),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.all(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
consumeOutsideTap: true,
|
||||||
|
menuChildren: RemoteAlbumSortMode.values
|
||||||
|
.map(
|
||||||
|
(sortMode) => MenuItemButton(
|
||||||
|
leadingIcon: albumSortOption == sortMode
|
||||||
|
? albumSortIsReverse
|
||||||
|
? Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: albumSortOption == sortMode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.keyboard_arrow_up_rounded,
|
||||||
|
color: albumSortOption == sortMode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
: const Icon(Icons.abc, color: Colors.transparent),
|
||||||
|
onPressed: () => onMenuTapped(sortMode),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(
|
||||||
|
const EdgeInsets.fromLTRB(16, 16, 32, 16),
|
||||||
|
),
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
albumSortOption == sortMode
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
sortMode.key.t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: albumSortOption == sortMode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface.withAlpha(185),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (controller.isOpen) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: albumSortIsReverse
|
||||||
|
? const Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.keyboard_arrow_up_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
albumSortOption.key.t(context: context),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(225),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchBar extends StatelessWidget {
|
||||||
|
const _SearchBar({
|
||||||
|
required this.searchController,
|
||||||
|
required this.searchFocusNode,
|
||||||
|
required this.onSearch,
|
||||||
|
required this.filterMode,
|
||||||
|
required this.onClearSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController searchController;
|
||||||
|
final FocusNode searchFocusNode;
|
||||||
|
final void Function(String, QuickFilterMode) onSearch;
|
||||||
|
final QuickFilterMode filterMode;
|
||||||
|
final VoidCallback onClearSearch;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||||
|
context.colorScheme.primary.withValues(alpha: 0.09),
|
||||||
|
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
transform: const GradientRotation(0.5 * pi),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SearchField(
|
||||||
|
autofocus: false,
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
hintText: 'search_albums'.tr(),
|
||||||
|
prefixIcon: const Icon(Icons.search_rounded),
|
||||||
|
suffixIcon: searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear_rounded),
|
||||||
|
onPressed: onClearSearch,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: (_) => onSearch(searchController.text, filterMode),
|
||||||
|
focusNode: searchFocusNode,
|
||||||
|
onTapOutside: (_) => searchFocusNode.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickFilterButtonRow extends StatelessWidget {
|
||||||
|
const _QuickFilterButtonRow({
|
||||||
|
required this.filterMode,
|
||||||
|
required this.onChangeFilter,
|
||||||
|
required this.onSearch,
|
||||||
|
required this.searchController,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuickFilterMode filterMode;
|
||||||
|
final void Function(QuickFilterMode) onChangeFilter;
|
||||||
|
final void Function(String, QuickFilterMode) onSearch;
|
||||||
|
final TextEditingController searchController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_QuickFilterButton(
|
||||||
|
label: 'all'.tr(),
|
||||||
|
isSelected: filterMode == QuickFilterMode.all,
|
||||||
|
onTap: () {
|
||||||
|
onChangeFilter(QuickFilterMode.all);
|
||||||
|
onSearch(searchController.text, QuickFilterMode.all);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_QuickFilterButton(
|
||||||
|
label: 'shared_with_me'.tr(),
|
||||||
|
isSelected: filterMode == QuickFilterMode.sharedWithMe,
|
||||||
|
onTap: () {
|
||||||
|
onChangeFilter(QuickFilterMode.sharedWithMe);
|
||||||
|
onSearch(
|
||||||
|
searchController.text,
|
||||||
|
QuickFilterMode.sharedWithMe,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_QuickFilterButton(
|
||||||
|
label: 'my_albums'.tr(),
|
||||||
|
isSelected: filterMode == QuickFilterMode.myAlbums,
|
||||||
|
onTap: () {
|
||||||
|
onChangeFilter(QuickFilterMode.myAlbums);
|
||||||
|
onSearch(
|
||||||
|
searchController.text,
|
||||||
|
QuickFilterMode.myAlbums,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickFilterButton extends StatelessWidget {
|
||||||
|
const _QuickFilterButton({
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(25),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickSortAndViewMode extends StatelessWidget {
|
||||||
|
const _QuickSortAndViewMode({
|
||||||
|
required this.isGrid,
|
||||||
|
required this.onToggleViewMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isGrid;
|
||||||
|
final VoidCallback onToggleViewMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const _SortButton(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: onToggleViewMode,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumList extends StatelessWidget {
|
||||||
|
const _AlbumList({
|
||||||
|
required this.isLoading,
|
||||||
|
required this.error,
|
||||||
|
required this.albums,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isLoading;
|
||||||
|
final String? error;
|
||||||
|
final List<Album> albums;
|
||||||
|
final String? userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (isLoading) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Text(
|
||||||
|
'Error loading albums: $error',
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: Text('No albums found'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
sliver: SliverList.builder(
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final album = albums[index];
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 8.0,
|
||||||
|
),
|
||||||
|
child: LargeLeadingTile(
|
||||||
|
title: Text(
|
||||||
|
album.name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${'items_count'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'count': album.assetCount,
|
||||||
|
},
|
||||||
|
)} • ${album.ownerId != userId ? 'shared_by_user'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'user': album.ownerName,
|
||||||
|
},
|
||||||
|
) : 'owned'.t(context: context)}',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context.router.push(
|
||||||
|
RemoteTimelineRoute(albumId: album.id),
|
||||||
|
),
|
||||||
|
leadingPadding: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: album.thumbnailAssetId != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(15),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: Thumbnail(
|
||||||
|
remoteId: album.thumbnailAssetId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: Icon(
|
||||||
|
Icons.photo_album_rounded,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: albums.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumGrid extends StatelessWidget {
|
||||||
|
const _AlbumGrid({
|
||||||
|
required this.albums,
|
||||||
|
required this.userId,
|
||||||
|
required this.isLoading,
|
||||||
|
required this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Album> albums;
|
||||||
|
final String? userId;
|
||||||
|
final bool isLoading;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (isLoading) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Text(
|
||||||
|
'Error loading albums: $error',
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albums.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: Text('No albums found'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 250,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: .7,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final album = albums[index];
|
||||||
|
return _GridAlbumCard(
|
||||||
|
album: album,
|
||||||
|
userId: userId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: albums.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GridAlbumCard extends StatelessWidget {
|
||||||
|
const _GridAlbumCard({
|
||||||
|
required this.album,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Album album;
|
||||||
|
final String? userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.router.push(
|
||||||
|
RemoteTimelineRoute(albumId: album.id),
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: context.colorScheme.surfaceBright,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(25),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(15),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: album.thumbnailAssetId != null
|
||||||
|
? Thumbnail(
|
||||||
|
remoteId: album.thumbnailAssetId,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
color: context.colorScheme.surfaceContainerHighest,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.photo_album_rounded,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
album.name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${'items_count'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'count': album.assetCount,
|
||||||
|
},
|
||||||
|
)} • ${album.ownerId != userId ? 'shared_by_user'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'user': album.ownerName,
|
||||||
|
},
|
||||||
|
) : 'owned'.t(context: context)}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,51 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class ArchiveActionButton extends ConsumerWidget {
|
class ArchiveActionButton extends ConsumerWidget {
|
||||||
const ArchiveActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const ArchiveActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).archive(source);
|
||||||
|
await ref.read(timelineServiceProvider).reloadBucket();
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'archive_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.archive_outlined,
|
iconData: Icons.archive_outlined,
|
||||||
label: "archive".t(context: context),
|
label: "archive".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-1
@@ -1,16 +1,54 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class EditLocationActionButton extends ConsumerWidget {
|
class EditLocationActionButton extends ConsumerWidget {
|
||||||
const EditLocationActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const EditLocationActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
_onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await ref.read(actionProvider.notifier).editLocation(source, context);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'edit_location_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.edit_location_alt_outlined,
|
iconData: Icons.edit_location_alt_outlined,
|
||||||
label: "control_bottom_app_bar_edit_location".t(context: context),
|
label: "control_bottom_app_bar_edit_location".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,51 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class FavoriteActionButton extends ConsumerWidget {
|
class FavoriteActionButton extends ConsumerWidget {
|
||||||
const FavoriteActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const FavoriteActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).favorite(source);
|
||||||
|
await ref.read(timelineServiceProvider).reloadBucket();
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'favorite_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.favorite_border_rounded,
|
iconData: Icons.favorite_border_rounded,
|
||||||
label: "favorite".t(context: context),
|
label: "favorite".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-1
@@ -1,10 +1,45 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class MoveToLockFolderActionButton extends ConsumerWidget {
|
class MoveToLockFolderActionButton extends ConsumerWidget {
|
||||||
const MoveToLockFolderActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const MoveToLockFolderActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
||||||
|
await ref.read(timelineServiceProvider).reloadBucket();
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -12,6 +47,7 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 100.0,
|
maxWidth: 100.0,
|
||||||
iconData: Icons.lock_outline_rounded,
|
iconData: Icons.lock_outline_rounded,
|
||||||
label: "move_to_locked_folder".t(context: context),
|
label: "move_to_locked_folder".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-1
@@ -1,10 +1,45 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
||||||
const RemoveFromLockFolderActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const RemoveFromLockFolderActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await ref.read(actionProvider.notifier).removeFromLockFolder(source);
|
||||||
|
await ref.read(timelineServiceProvider).reloadBucket();
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'remove_from_lock_folder_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -12,6 +47,7 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 100.0,
|
maxWidth: 100.0,
|
||||||
iconData: Icons.lock_open_rounded,
|
iconData: Icons.lock_open_rounded,
|
||||||
label: "remove_from_locked_folder".t(context: context),
|
label: "remove_from_locked_folder".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-1
@@ -1,16 +1,50 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class ShareLinkActionButton extends ConsumerWidget {
|
class ShareLinkActionButton extends ConsumerWidget {
|
||||||
const ShareLinkActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const ShareLinkActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
_onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await ref.read(actionProvider.notifier).shareLink(source, context);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'share_link_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.link_rounded,
|
iconData: Icons.link_rounded,
|
||||||
label: "share_link".t(context: context),
|
label: "share_link".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +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/constants/enums.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
@@ -29,20 +30,22 @@ class HomeBottomAppBar extends ConsumerWidget {
|
|||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
initialChildSize: 0.25,
|
initialChildSize: 0.25,
|
||||||
minChildSize: 0.22,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
const ShareActionButton(),
|
if (multiselect.isEnabled) const ShareActionButton(),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(),
|
const DownloadActionButton(),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton()
|
? const TrashActionButton()
|
||||||
: const DeletePermanentActionButton(),
|
: const DeletePermanentActionButton(),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(),
|
||||||
const EditLocationActionButton(),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(),
|
const MoveToLockFolderActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
const StackActionButton(),
|
const StackActionButton(),
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal) ...[
|
||||||
|
|||||||
@@ -10,20 +10,39 @@ import 'package:octo_image/octo_image.dart';
|
|||||||
|
|
||||||
class Thumbnail extends StatelessWidget {
|
class Thumbnail extends StatelessWidget {
|
||||||
const Thumbnail({
|
const Thumbnail({
|
||||||
required this.asset,
|
this.asset,
|
||||||
|
this.remoteId,
|
||||||
this.size = const Size.square(256),
|
this.size = const Size.square(256),
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
}) : assert(
|
||||||
|
asset != null || remoteId != null,
|
||||||
|
'Either asset or remoteId must be provided',
|
||||||
|
);
|
||||||
|
|
||||||
final BaseAsset asset;
|
final BaseAsset? asset;
|
||||||
|
final String? remoteId;
|
||||||
final Size size;
|
final Size size;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
|
||||||
static ImageProvider imageProvider({
|
static ImageProvider imageProvider({
|
||||||
required BaseAsset asset,
|
BaseAsset? asset,
|
||||||
|
String? remoteId,
|
||||||
Size size = const Size.square(256),
|
Size size = const Size.square(256),
|
||||||
}) {
|
}) {
|
||||||
|
assert(
|
||||||
|
asset != null || remoteId != null,
|
||||||
|
'Either asset or remoteId must be provided',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remoteId != null) {
|
||||||
|
return RemoteThumbProvider(
|
||||||
|
assetId: remoteId,
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (asset is LocalAsset) {
|
if (asset is LocalAsset) {
|
||||||
return LocalThumbProvider(
|
return LocalThumbProvider(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
@@ -32,7 +51,7 @@ class Thumbnail extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset is Asset) {
|
if (asset is RemoteAsset) {
|
||||||
return RemoteThumbProvider(
|
return RemoteThumbProvider(
|
||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
@@ -45,8 +64,10 @@ class Thumbnail extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbHash = asset is Asset ? (asset as Asset).thumbHash : null;
|
final thumbHash =
|
||||||
final provider = imageProvider(asset: asset, size: size);
|
asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||||
|
final provider =
|
||||||
|
imageProvider(asset: asset, remoteId: remoteId, size: size);
|
||||||
|
|
||||||
return OctoImage.fromSet(
|
return OctoImage.fromSet(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
? context.primaryColor.darken(amount: 0.6)
|
? context.primaryColor.darken(amount: 0.6)
|
||||||
: context.primaryColor.lighten(amount: 0.8);
|
: context.primaryColor.lighten(amount: 0.8);
|
||||||
|
|
||||||
final isSelected = ref
|
final isSelected = ref.watch(
|
||||||
.watch(multiSelectProvider.select((state) => state.selectedAssets))
|
multiSelectProvider.select(
|
||||||
.contains(asset);
|
(multiselect) => multiselect.selectedAssets.contains(asset),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class FixedSegment extends Segment {
|
|||||||
/// and prevents duplicate keys even when assets have the same name/timestamp
|
/// and prevents duplicate keys even when assets have the same name/timestamp
|
||||||
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
|
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
|
||||||
// Try to get the most unique identifier based on asset type
|
// Try to get the most unique identifier based on asset type
|
||||||
if (asset is Asset) {
|
if (asset is RemoteAsset) {
|
||||||
// For remote/merged assets, use the remote ID which is globally unique
|
// For remote/merged assets, use the remote ID which is globally unique
|
||||||
return 'asset_${asset.id}';
|
return 'asset_${asset.id}';
|
||||||
} else if (asset is LocalAsset) {
|
} else if (asset is LocalAsset) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:immich_mobile/providers/haptic_feedback.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/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class TimelineHeader extends ConsumerWidget {
|
class TimelineHeader extends StatelessWidget {
|
||||||
final Bucket bucket;
|
final Bucket bucket;
|
||||||
final HeaderType header;
|
final HeaderType header;
|
||||||
final double height;
|
final double height;
|
||||||
@@ -36,23 +36,13 @@ class TimelineHeader extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
if (bucket is! TimeBucket || header == HeaderType.none) {
|
if (bucket is! TimeBucket || header == HeaderType.none) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final date = (bucket as TimeBucket).date;
|
final date = (bucket as TimeBucket).date;
|
||||||
|
|
||||||
List<BaseAsset> bucketAssets;
|
|
||||||
try {
|
|
||||||
bucketAssets = ref
|
|
||||||
.watch(timelineServiceProvider)
|
|
||||||
.getAssets(assetOffset, bucket.assetCount);
|
|
||||||
} catch (e) {
|
|
||||||
bucketAssets = <BaseAsset>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
|
|
||||||
final isMonthHeader =
|
final isMonthHeader =
|
||||||
header == HeaderType.month || header == HeaderType.monthAndDay;
|
header == HeaderType.month || header == HeaderType.monthAndDay;
|
||||||
final isDayHeader =
|
final isDayHeader =
|
||||||
@@ -80,16 +70,8 @@ class TimelineHeader extends ConsumerWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (header != HeaderType.monthAndDay)
|
if (header != HeaderType.monthAndDay)
|
||||||
_BulkSelectIconButton(
|
_BulkSelectIconButton(
|
||||||
isAllSelected: isAllSelected,
|
bucket: bucket,
|
||||||
onPressed: () {
|
assetOffset: assetOffset,
|
||||||
ref
|
|
||||||
.read(multiSelectProvider.notifier)
|
|
||||||
.toggleBucketSelection(
|
|
||||||
assetOffset,
|
|
||||||
bucket.assetCount,
|
|
||||||
);
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -104,16 +86,8 @@ class TimelineHeader extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_BulkSelectIconButton(
|
_BulkSelectIconButton(
|
||||||
isAllSelected: isAllSelected,
|
bucket: bucket,
|
||||||
onPressed: () {
|
assetOffset: assetOffset,
|
||||||
ref
|
|
||||||
.read(multiSelectProvider.notifier)
|
|
||||||
.toggleBucketSelection(
|
|
||||||
assetOffset,
|
|
||||||
bucket.assetCount,
|
|
||||||
);
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -125,18 +99,35 @@ class TimelineHeader extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BulkSelectIconButton extends ConsumerWidget {
|
class _BulkSelectIconButton extends ConsumerWidget {
|
||||||
final bool isAllSelected;
|
final Bucket bucket;
|
||||||
final VoidCallback onPressed;
|
final int assetOffset;
|
||||||
|
|
||||||
const _BulkSelectIconButton({
|
const _BulkSelectIconButton({
|
||||||
required this.isAllSelected,
|
required this.bucket,
|
||||||
required this.onPressed,
|
required this.assetOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
List<BaseAsset> bucketAssets;
|
||||||
|
try {
|
||||||
|
bucketAssets = ref
|
||||||
|
.watch(timelineServiceProvider)
|
||||||
|
.getAssets(assetOffset, bucket.assetCount);
|
||||||
|
} catch (e) {
|
||||||
|
bucketAssets = <BaseAsset>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
|
||||||
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: onPressed,
|
onPressed: () {
|
||||||
|
ref.read(multiSelectProvider.notifier).toggleBucketSelection(
|
||||||
|
assetOffset,
|
||||||
|
bucket.assetCount,
|
||||||
|
);
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
|
},
|
||||||
icon: isAllSelected
|
icon: isAllSelected
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.check_circle_rounded,
|
Icons.check_circle_rounded,
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ class _MultiSelectStatusButton extends ConsumerWidget {
|
|||||||
final selectCount =
|
final selectCount =
|
||||||
ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
|
ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
|
||||||
return ElevatedButton.icon(
|
return ElevatedButton.icon(
|
||||||
onPressed: () => ref.read(multiSelectProvider.notifier).clearSelection(),
|
onPressed: () => ref.read(multiSelectProvider.notifier).reset(),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.close_rounded,
|
Icons.close_rounded,
|
||||||
color: context.colorScheme.onPrimary,
|
color: context.colorScheme.onPrimary,
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/action.service.dart';
|
||||||
|
import 'package:immich_mobile/services/timeline.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
final actionProvider = NotifierProvider<ActionNotifier, void>(
|
||||||
|
ActionNotifier.new,
|
||||||
|
dependencies: [
|
||||||
|
multiSelectProvider,
|
||||||
|
timelineServiceProvider,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
class ActionResult {
|
||||||
|
final int count;
|
||||||
|
final bool success;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const ActionResult({required this.count, required this.success, this.error});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'ActionResult(count: $count, success: $success, error: $error)';
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionNotifier extends Notifier<void> {
|
||||||
|
final Logger _logger = Logger('ActionNotifier');
|
||||||
|
late ActionService _service;
|
||||||
|
|
||||||
|
ActionNotifier() : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {
|
||||||
|
_service = ref.watch(actionServiceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (T is RemoteAsset && currentUser == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<BaseAsset> assets = switch (source) {
|
||||||
|
ActionSource.timeline =>
|
||||||
|
ref.read(multiSelectProvider.select((s) => s.selectedAssets)),
|
||||||
|
ActionSource.viewer => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
const (RemoteAsset) => assets
|
||||||
|
.where(
|
||||||
|
(asset) => asset is RemoteAsset && asset.ownerId == currentUser!.id,
|
||||||
|
)
|
||||||
|
.cast<RemoteAsset>()
|
||||||
|
.map((asset) => asset.id)
|
||||||
|
.toList(),
|
||||||
|
const (LocalAsset) =>
|
||||||
|
assets.whereType<LocalAsset>().map((asset) => asset.id).toList(),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> shareLink(
|
||||||
|
ActionSource source,
|
||||||
|
BuildContext context,
|
||||||
|
) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.shareLink(ids, context);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to create shared link for assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> favorite(ActionSource source) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.favorite(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to favorite assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> unFavorite(ActionSource source) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.unFavorite(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to unfavorite assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> archive(ActionSource source) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.archive(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to archive assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> unArchive(ActionSource source) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.unArchive(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to unarchive assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.moveToLockFolder(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to move assets to lock folder', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> removeFromLockFolder(ActionSource source) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
await _service.removeFromLockFolder(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to remove assets from lock folder', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult?> editLocation(
|
||||||
|
ActionSource source,
|
||||||
|
BuildContext context,
|
||||||
|
) async {
|
||||||
|
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||||
|
try {
|
||||||
|
final isEdited = await _service.editLocation(ids, context);
|
||||||
|
if (!isEdited) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to edit location for assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/remote_album.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/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
|
||||||
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
||||||
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
||||||
@@ -10,3 +12,14 @@ final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
|||||||
final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
|
final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
|
||||||
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
|
||||||
|
(ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository)),
|
||||||
|
dependencies: [remoteAlbumRepository],
|
||||||
|
);
|
||||||
|
|
||||||
|
final remoteAlbumProvider =
|
||||||
|
NotifierProvider<RemoteAlbumNotifier, RemoteAlbumState>(
|
||||||
|
RemoteAlbumNotifier.new,
|
||||||
|
dependencies: [remoteAlbumServiceProvider],
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
final localAssetRepository = Provider<DriftLocalAssetRepository>(
|
final localAssetRepository = Provider<DriftLocalAssetRepository>(
|
||||||
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final remoteAssetRepository = Provider<DriftRemoteAssetRepository>(
|
||||||
|
(ref) => DriftRemoteAssetRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|||||||
@@ -8,3 +8,7 @@ part 'exif.provider.g.dart';
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
IsarExifRepository exifRepository(Ref ref) =>
|
IsarExifRepository exifRepository(Ref ref) =>
|
||||||
IsarExifRepository(ref.watch(isarProvider));
|
IsarExifRepository(ref.watch(isarProvider));
|
||||||
|
|
||||||
|
final remoteExifRepository = Provider<DriftRemoteExifRepository>(
|
||||||
|
(ref) => DriftRemoteExifRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.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/utils/remote_album.utils.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import 'album.provider.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumState {
|
||||||
|
final List<Album> albums;
|
||||||
|
final List<Album> filteredAlbums;
|
||||||
|
final bool isLoading;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const RemoteAlbumState({
|
||||||
|
required this.albums,
|
||||||
|
List<Album>? filteredAlbums,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.error,
|
||||||
|
}) : filteredAlbums = filteredAlbums ?? albums;
|
||||||
|
|
||||||
|
RemoteAlbumState copyWith({
|
||||||
|
List<Album>? albums,
|
||||||
|
List<Album>? filteredAlbums,
|
||||||
|
bool? isLoading,
|
||||||
|
String? error,
|
||||||
|
}) {
|
||||||
|
return RemoteAlbumState(
|
||||||
|
albums: albums ?? this.albums,
|
||||||
|
filteredAlbums: filteredAlbums ?? this.filteredAlbums,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
error: error ?? this.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length}, isLoading: $isLoading, error: $error)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant RemoteAlbumState other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return listEquals(other.albums, albums) &&
|
||||||
|
listEquals(other.filteredAlbums, filteredAlbums) &&
|
||||||
|
other.isLoading == isLoading &&
|
||||||
|
other.error == error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
albums.hashCode ^
|
||||||
|
filteredAlbums.hashCode ^
|
||||||
|
isLoading.hashCode ^
|
||||||
|
error.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||||
|
late final RemoteAlbumService _remoteAlbumService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RemoteAlbumState build() {
|
||||||
|
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
||||||
|
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Album>> getAll() async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final albums = await _remoteAlbumService.getAll();
|
||||||
|
state = state.copyWith(
|
||||||
|
albums: albums,
|
||||||
|
filteredAlbums: albums,
|
||||||
|
isLoading: false,
|
||||||
|
);
|
||||||
|
return albums;
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void searchAlbums(
|
||||||
|
String query,
|
||||||
|
String? userId, [
|
||||||
|
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||||
|
]) {
|
||||||
|
final filtered = _remoteAlbumService.searchAlbums(
|
||||||
|
state.albums,
|
||||||
|
query,
|
||||||
|
userId,
|
||||||
|
filterMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
filteredAlbums: filtered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSearch() {
|
||||||
|
state = state.copyWith(
|
||||||
|
filteredAlbums: state.albums,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortFilteredAlbums(
|
||||||
|
RemoteAlbumSortMode sortMode, {
|
||||||
|
bool isReverse = false,
|
||||||
|
}) {
|
||||||
|
final sortedAlbums = _remoteAlbumService
|
||||||
|
.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
||||||
|
state = state.copyWith(filteredAlbums: sortedAlbums);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,17 @@ final timelineArgsProvider = Provider.autoDispose<TimelineArgs>(
|
|||||||
throw UnimplementedError('Will be overridden through a ProviderScope.'),
|
throw UnimplementedError('Will be overridden through a ProviderScope.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final timelineServiceProvider = Provider.autoDispose<TimelineService>(
|
final timelineServiceProvider = Provider<TimelineService>(
|
||||||
(ref) =>
|
(ref) {
|
||||||
throw UnimplementedError('Will be overridden through a ProviderScope.'),
|
final timelineUsers = ref.watch(timelineUsersProvider).valueOrNull ?? [];
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).main(timelineUsers);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
// Empty dependencies to inform the framework that this provider
|
||||||
|
// might be used in a ProviderScope
|
||||||
|
dependencies: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
final timelineFactoryProvider = Provider<TimelineFactory>(
|
final timelineFactoryProvider = Provider<TimelineFactory>(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.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/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@@ -14,9 +13,7 @@ final multiSelectProvider =
|
|||||||
class MultiSelectState {
|
class MultiSelectState {
|
||||||
final Set<BaseAsset> selectedAssets;
|
final Set<BaseAsset> selectedAssets;
|
||||||
|
|
||||||
const MultiSelectState({
|
const MultiSelectState({required this.selectedAssets});
|
||||||
required this.selectedAssets,
|
|
||||||
});
|
|
||||||
|
|
||||||
bool get isEnabled => selectedAssets.isNotEmpty;
|
bool get isEnabled => selectedAssets.isNotEmpty;
|
||||||
bool get hasRemote => selectedAssets.any(
|
bool get hasRemote => selectedAssets.any(
|
||||||
@@ -28,9 +25,7 @@ class MultiSelectState {
|
|||||||
(asset) => asset.storage == AssetState.local,
|
(asset) => asset.storage == AssetState.local,
|
||||||
);
|
);
|
||||||
|
|
||||||
MultiSelectState copyWith({
|
MultiSelectState copyWith({Set<BaseAsset>? selectedAssets}) {
|
||||||
Set<BaseAsset>? selectedAssets,
|
|
||||||
}) {
|
|
||||||
return MultiSelectState(
|
return MultiSelectState(
|
||||||
selectedAssets: selectedAssets ?? this.selectedAssets,
|
selectedAssets: selectedAssets ?? this.selectedAssets,
|
||||||
);
|
);
|
||||||
@@ -52,15 +47,11 @@ class MultiSelectState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MultiSelectNotifier extends Notifier<MultiSelectState> {
|
class MultiSelectNotifier extends Notifier<MultiSelectState> {
|
||||||
late final TimelineService _timelineService;
|
TimelineService get _timelineService => ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MultiSelectState build() {
|
MultiSelectState build() {
|
||||||
_timelineService = ref.read(timelineServiceProvider);
|
return const MultiSelectState(selectedAssets: {});
|
||||||
|
|
||||||
return const MultiSelectState(
|
|
||||||
selectedAssets: {},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectAsset(BaseAsset asset) {
|
void selectAsset(BaseAsset asset) {
|
||||||
@@ -91,10 +82,8 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSelection() {
|
void reset() {
|
||||||
state = state.copyWith(
|
state = const MultiSelectState(selectedAssets: {});
|
||||||
selectedAssets: {},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bucket bulk operations
|
/// Bucket bulk operations
|
||||||
@@ -154,5 +143,5 @@ final bucketSelectionProvider = Provider.family<bool, List<BaseAsset>>(
|
|||||||
// Check if all assets in the bucket are selected
|
// Check if all assets in the bucket are selected
|
||||||
return bucketAssets.every((asset) => selectedAssets.contains(asset));
|
return bucketAssets.every((asset) => selectedAssets.contains(asset));
|
||||||
},
|
},
|
||||||
dependencies: [multiSelectProvider],
|
dependencies: [multiSelectProvider, timelineServiceProvider],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final assetApiRepositoryProvider = Provider(
|
final assetApiRepositoryProvider = Provider(
|
||||||
@@ -56,6 +57,28 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateFavorite(
|
||||||
|
List<String> ids,
|
||||||
|
bool isFavorite,
|
||||||
|
) async {
|
||||||
|
return _api.updateAssets(
|
||||||
|
AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateLocation(
|
||||||
|
List<String> ids,
|
||||||
|
LatLng location,
|
||||||
|
) async {
|
||||||
|
return _api.updateAssets(
|
||||||
|
AssetBulkUpdateDto(
|
||||||
|
ids: ids,
|
||||||
|
latitude: location.latitude,
|
||||||
|
longitude: location.longitude,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
@@ -172,7 +173,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: AlbumsRoute.page,
|
page: DriftAlbumsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -550,6 +550,22 @@ class CropImageRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftAlbumsPage]
|
||||||
|
class DriftAlbumsRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftAlbumsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftAlbumsRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftAlbumsRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftAlbumsPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [EditImagePage]
|
/// [EditImagePage]
|
||||||
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
final actionServiceProvider = Provider<ActionService>(
|
||||||
|
(ref) => ActionService(
|
||||||
|
ref.watch(assetApiRepositoryProvider),
|
||||||
|
ref.watch(remoteAssetRepository),
|
||||||
|
ref.watch(remoteExifRepository),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ActionService {
|
||||||
|
final AssetApiRepository _assetApiRepository;
|
||||||
|
final DriftRemoteAssetRepository _remoteAssetRepository;
|
||||||
|
final DriftRemoteExifRepository _remoteExifRepository;
|
||||||
|
|
||||||
|
const ActionService(
|
||||||
|
this._assetApiRepository,
|
||||||
|
this._remoteAssetRepository,
|
||||||
|
this._remoteExifRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||||
|
context.pushRoute(
|
||||||
|
SharedLinkEditRoute(
|
||||||
|
assetsList: remoteIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> favorite(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.updateFavorite(remoteIds, true);
|
||||||
|
await _remoteAssetRepository.updateFavorite(remoteIds, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unFavorite(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.updateFavorite(remoteIds, false);
|
||||||
|
await _remoteAssetRepository.updateFavorite(remoteIds, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> archive(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibilityEnum.archive,
|
||||||
|
);
|
||||||
|
await _remoteAssetRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibility.archive,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unArchive(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibilityEnum.timeline,
|
||||||
|
);
|
||||||
|
await _remoteAssetRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibility.timeline,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> moveToLockFolder(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibilityEnum.locked,
|
||||||
|
);
|
||||||
|
await _remoteAssetRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibility.locked,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeFromLockFolder(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibilityEnum.timeline,
|
||||||
|
);
|
||||||
|
await _remoteAssetRepository.updateVisibility(
|
||||||
|
remoteIds,
|
||||||
|
AssetVisibility.timeline,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> editLocation(
|
||||||
|
List<String> remoteIds,
|
||||||
|
BuildContext context,
|
||||||
|
) async {
|
||||||
|
LatLng? initialLatLng;
|
||||||
|
if (remoteIds.length == 1) {
|
||||||
|
final exif = await _remoteExifRepository.get(remoteIds[0]);
|
||||||
|
|
||||||
|
if (exif?.latitude != null && exif?.longitude != null) {
|
||||||
|
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final location = await showLocationPicker(
|
||||||
|
context: context,
|
||||||
|
initialLatLng: initialLatLng,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (location == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _assetApiRepository.updateLocation(
|
||||||
|
remoteIds,
|
||||||
|
location,
|
||||||
|
);
|
||||||
|
await _remoteAssetRepository.updateLocation(
|
||||||
|
remoteIds,
|
||||||
|
location,
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
|
||||||
|
typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
|
||||||
|
|
||||||
|
class _RemoteAlbumSortHandlers {
|
||||||
|
const _RemoteAlbumSortHandlers._();
|
||||||
|
|
||||||
|
static const AlbumSortFn created = _sortByCreated;
|
||||||
|
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
|
||||||
|
final sorted = albums.sortedBy((album) => album.createdAt);
|
||||||
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const AlbumSortFn title = _sortByTitle;
|
||||||
|
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
|
||||||
|
final sorted = albums.sortedBy((album) => album.name);
|
||||||
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const AlbumSortFn lastModified = _sortByLastModified;
|
||||||
|
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
|
||||||
|
final sorted = albums.sortedBy((album) => album.updatedAt);
|
||||||
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const AlbumSortFn assetCount = _sortByAssetCount;
|
||||||
|
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
|
||||||
|
final sorted =
|
||||||
|
albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
|
||||||
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const AlbumSortFn mostRecent = _sortByMostRecent;
|
||||||
|
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
|
||||||
|
final sorted = albums.sorted((a, b) {
|
||||||
|
// For most recent, we sort by updatedAt in descending order
|
||||||
|
return b.updatedAt.compareTo(a.updatedAt);
|
||||||
|
});
|
||||||
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const AlbumSortFn mostOldest = _sortByMostOldest;
|
||||||
|
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
|
||||||
|
final sorted = albums.sorted((a, b) {
|
||||||
|
// For oldest, we sort by createdAt in ascending order
|
||||||
|
return a.createdAt.compareTo(b.createdAt);
|
||||||
|
});
|
||||||
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RemoteAlbumSortMode {
|
||||||
|
title("library_page_sort_title", _RemoteAlbumSortHandlers.title),
|
||||||
|
assetCount(
|
||||||
|
"library_page_sort_asset_count",
|
||||||
|
_RemoteAlbumSortHandlers.assetCount,
|
||||||
|
),
|
||||||
|
lastModified(
|
||||||
|
"library_page_sort_last_modified",
|
||||||
|
_RemoteAlbumSortHandlers.lastModified,
|
||||||
|
),
|
||||||
|
created("library_page_sort_created", _RemoteAlbumSortHandlers.created),
|
||||||
|
mostRecent("sort_recent", _RemoteAlbumSortHandlers.mostRecent),
|
||||||
|
mostOldest("sort_oldest", _RemoteAlbumSortHandlers.mostOldest);
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
final AlbumSortFn sortFn;
|
||||||
|
|
||||||
|
const RemoteAlbumSortMode(this.key, this.sortFn);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
@@ -62,8 +63,14 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.science_rounded),
|
icon: const Icon(Icons.swipe_left_alt_rounded),
|
||||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.sync,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isCasting)
|
if (isCasting)
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
Generated
+2
@@ -482,6 +482,8 @@ Class | Method | HTTP request | Description
|
|||||||
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
|
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
|
||||||
- [SyncPartnerV1](doc//SyncPartnerV1.md)
|
- [SyncPartnerV1](doc//SyncPartnerV1.md)
|
||||||
- [SyncRequestType](doc//SyncRequestType.md)
|
- [SyncRequestType](doc//SyncRequestType.md)
|
||||||
|
- [SyncStackDeleteV1](doc//SyncStackDeleteV1.md)
|
||||||
|
- [SyncStackV1](doc//SyncStackV1.md)
|
||||||
- [SyncStreamDto](doc//SyncStreamDto.md)
|
- [SyncStreamDto](doc//SyncStreamDto.md)
|
||||||
- [SyncUserDeleteV1](doc//SyncUserDeleteV1.md)
|
- [SyncUserDeleteV1](doc//SyncUserDeleteV1.md)
|
||||||
- [SyncUserV1](doc//SyncUserV1.md)
|
- [SyncUserV1](doc//SyncUserV1.md)
|
||||||
|
|||||||
Generated
+2
@@ -265,6 +265,8 @@ part 'model/sync_memory_v1.dart';
|
|||||||
part 'model/sync_partner_delete_v1.dart';
|
part 'model/sync_partner_delete_v1.dart';
|
||||||
part 'model/sync_partner_v1.dart';
|
part 'model/sync_partner_v1.dart';
|
||||||
part 'model/sync_request_type.dart';
|
part 'model/sync_request_type.dart';
|
||||||
|
part 'model/sync_stack_delete_v1.dart';
|
||||||
|
part 'model/sync_stack_v1.dart';
|
||||||
part 'model/sync_stream_dto.dart';
|
part 'model/sync_stream_dto.dart';
|
||||||
part 'model/sync_user_delete_v1.dart';
|
part 'model/sync_user_delete_v1.dart';
|
||||||
part 'model/sync_user_v1.dart';
|
part 'model/sync_user_v1.dart';
|
||||||
|
|||||||
Generated
+4
@@ -586,6 +586,10 @@ class ApiClient {
|
|||||||
return SyncPartnerV1.fromJson(value);
|
return SyncPartnerV1.fromJson(value);
|
||||||
case 'SyncRequestType':
|
case 'SyncRequestType':
|
||||||
return SyncRequestTypeTypeTransformer().decode(value);
|
return SyncRequestTypeTypeTransformer().decode(value);
|
||||||
|
case 'SyncStackDeleteV1':
|
||||||
|
return SyncStackDeleteV1.fromJson(value);
|
||||||
|
case 'SyncStackV1':
|
||||||
|
return SyncStackV1.fromJson(value);
|
||||||
case 'SyncStreamDto':
|
case 'SyncStreamDto':
|
||||||
return SyncStreamDto.fromJson(value);
|
return SyncStreamDto.fromJson(value);
|
||||||
case 'SyncUserDeleteV1':
|
case 'SyncUserDeleteV1':
|
||||||
|
|||||||
+10
-10
@@ -56,21 +56,21 @@ class SyncAssetExifV1 {
|
|||||||
|
|
||||||
String? exposureTime;
|
String? exposureTime;
|
||||||
|
|
||||||
int? fNumber;
|
double? fNumber;
|
||||||
|
|
||||||
int? fileSizeInByte;
|
int? fileSizeInByte;
|
||||||
|
|
||||||
int? focalLength;
|
double? focalLength;
|
||||||
|
|
||||||
int? fps;
|
double? fps;
|
||||||
|
|
||||||
int? iso;
|
int? iso;
|
||||||
|
|
||||||
int? latitude;
|
double? latitude;
|
||||||
|
|
||||||
String? lensModel;
|
String? lensModel;
|
||||||
|
|
||||||
int? longitude;
|
double? longitude;
|
||||||
|
|
||||||
String? make;
|
String? make;
|
||||||
|
|
||||||
@@ -293,14 +293,14 @@ class SyncAssetExifV1 {
|
|||||||
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
||||||
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
||||||
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
||||||
fNumber: mapValueOfType<int>(json, r'fNumber'),
|
fNumber: (mapValueOfType<num>(json, r'fNumber'))?.toDouble(),
|
||||||
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
||||||
focalLength: mapValueOfType<int>(json, r'focalLength'),
|
focalLength: (mapValueOfType<num>(json, r'focalLength'))?.toDouble(),
|
||||||
fps: mapValueOfType<int>(json, r'fps'),
|
fps: (mapValueOfType<num>(json, r'fps'))?.toDouble(),
|
||||||
iso: mapValueOfType<int>(json, r'iso'),
|
iso: mapValueOfType<int>(json, r'iso'),
|
||||||
latitude: mapValueOfType<int>(json, r'latitude'),
|
latitude: (mapValueOfType<num>(json, r'latitude'))?.toDouble(),
|
||||||
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
||||||
longitude: mapValueOfType<int>(json, r'longitude'),
|
longitude: (mapValueOfType<num>(json, r'longitude'))?.toDouble(),
|
||||||
make: mapValueOfType<String>(json, r'make'),
|
make: mapValueOfType<String>(json, r'make'),
|
||||||
model: mapValueOfType<String>(json, r'model'),
|
model: mapValueOfType<String>(json, r'model'),
|
||||||
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||||
|
|||||||
+15
@@ -35,6 +35,9 @@ class SyncEntityType {
|
|||||||
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
||||||
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
||||||
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
|
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
|
||||||
|
static const partnerStackBackfillV1 = SyncEntityType._(r'PartnerStackBackfillV1');
|
||||||
|
static const partnerStackDeleteV1 = SyncEntityType._(r'PartnerStackDeleteV1');
|
||||||
|
static const partnerStackV1 = SyncEntityType._(r'PartnerStackV1');
|
||||||
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
||||||
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
||||||
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||||
@@ -51,6 +54,8 @@ class SyncEntityType {
|
|||||||
static const memoryDeleteV1 = SyncEntityType._(r'MemoryDeleteV1');
|
static const memoryDeleteV1 = SyncEntityType._(r'MemoryDeleteV1');
|
||||||
static const memoryToAssetV1 = SyncEntityType._(r'MemoryToAssetV1');
|
static const memoryToAssetV1 = SyncEntityType._(r'MemoryToAssetV1');
|
||||||
static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1');
|
static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1');
|
||||||
|
static const stackV1 = SyncEntityType._(r'StackV1');
|
||||||
|
static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1');
|
||||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
@@ -67,6 +72,9 @@ class SyncEntityType {
|
|||||||
partnerAssetDeleteV1,
|
partnerAssetDeleteV1,
|
||||||
partnerAssetExifV1,
|
partnerAssetExifV1,
|
||||||
partnerAssetExifBackfillV1,
|
partnerAssetExifBackfillV1,
|
||||||
|
partnerStackBackfillV1,
|
||||||
|
partnerStackDeleteV1,
|
||||||
|
partnerStackV1,
|
||||||
albumV1,
|
albumV1,
|
||||||
albumDeleteV1,
|
albumDeleteV1,
|
||||||
albumUserV1,
|
albumUserV1,
|
||||||
@@ -83,6 +91,8 @@ class SyncEntityType {
|
|||||||
memoryDeleteV1,
|
memoryDeleteV1,
|
||||||
memoryToAssetV1,
|
memoryToAssetV1,
|
||||||
memoryToAssetDeleteV1,
|
memoryToAssetDeleteV1,
|
||||||
|
stackV1,
|
||||||
|
stackDeleteV1,
|
||||||
syncAckV1,
|
syncAckV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -134,6 +144,9 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
||||||
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
||||||
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
|
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
|
||||||
|
case r'PartnerStackBackfillV1': return SyncEntityType.partnerStackBackfillV1;
|
||||||
|
case r'PartnerStackDeleteV1': return SyncEntityType.partnerStackDeleteV1;
|
||||||
|
case r'PartnerStackV1': return SyncEntityType.partnerStackV1;
|
||||||
case r'AlbumV1': return SyncEntityType.albumV1;
|
case r'AlbumV1': return SyncEntityType.albumV1;
|
||||||
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
||||||
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||||
@@ -150,6 +163,8 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'MemoryDeleteV1': return SyncEntityType.memoryDeleteV1;
|
case r'MemoryDeleteV1': return SyncEntityType.memoryDeleteV1;
|
||||||
case r'MemoryToAssetV1': return SyncEntityType.memoryToAssetV1;
|
case r'MemoryToAssetV1': return SyncEntityType.memoryToAssetV1;
|
||||||
case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1;
|
case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1;
|
||||||
|
case r'StackV1': return SyncEntityType.stackV1;
|
||||||
|
case r'StackDeleteV1': return SyncEntityType.stackDeleteV1;
|
||||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
|
|||||||
+24
-18
@@ -23,35 +23,39 @@ class SyncRequestType {
|
|||||||
|
|
||||||
String toJson() => value;
|
String toJson() => value;
|
||||||
|
|
||||||
static const usersV1 = SyncRequestType._(r'UsersV1');
|
|
||||||
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
|
||||||
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
|
||||||
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
|
||||||
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
|
||||||
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
|
||||||
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
|
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
|
||||||
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||||
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
||||||
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
||||||
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
||||||
|
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
||||||
|
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||||
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
|
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
|
||||||
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
||||||
|
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
||||||
|
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
||||||
|
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||||
|
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
||||||
|
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
||||||
|
static const usersV1 = SyncRequestType._(r'UsersV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncRequestType].
|
/// List of all possible values in this [enum][SyncRequestType].
|
||||||
static const values = <SyncRequestType>[
|
static const values = <SyncRequestType>[
|
||||||
usersV1,
|
|
||||||
partnersV1,
|
|
||||||
assetsV1,
|
|
||||||
assetExifsV1,
|
|
||||||
partnerAssetsV1,
|
|
||||||
partnerAssetExifsV1,
|
|
||||||
albumsV1,
|
albumsV1,
|
||||||
albumUsersV1,
|
albumUsersV1,
|
||||||
albumToAssetsV1,
|
albumToAssetsV1,
|
||||||
albumAssetsV1,
|
albumAssetsV1,
|
||||||
albumAssetExifsV1,
|
albumAssetExifsV1,
|
||||||
|
assetsV1,
|
||||||
|
assetExifsV1,
|
||||||
memoriesV1,
|
memoriesV1,
|
||||||
memoryToAssetsV1,
|
memoryToAssetsV1,
|
||||||
|
partnersV1,
|
||||||
|
partnerAssetsV1,
|
||||||
|
partnerAssetExifsV1,
|
||||||
|
partnerStacksV1,
|
||||||
|
stacksV1,
|
||||||
|
usersV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
||||||
@@ -90,19 +94,21 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
SyncRequestType? decode(dynamic data, {bool allowNull = true}) {
|
SyncRequestType? decode(dynamic data, {bool allowNull = true}) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case r'UsersV1': return SyncRequestType.usersV1;
|
|
||||||
case r'PartnersV1': return SyncRequestType.partnersV1;
|
|
||||||
case r'AssetsV1': return SyncRequestType.assetsV1;
|
|
||||||
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
|
||||||
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
|
||||||
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
|
||||||
case r'AlbumsV1': return SyncRequestType.albumsV1;
|
case r'AlbumsV1': return SyncRequestType.albumsV1;
|
||||||
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||||
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
||||||
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
||||||
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
||||||
|
case r'AssetsV1': return SyncRequestType.assetsV1;
|
||||||
|
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||||
case r'MemoriesV1': return SyncRequestType.memoriesV1;
|
case r'MemoriesV1': return SyncRequestType.memoriesV1;
|
||||||
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
||||||
|
case r'PartnersV1': return SyncRequestType.partnersV1;
|
||||||
|
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
||||||
|
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||||
|
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
||||||
|
case r'StacksV1': return SyncRequestType.stacksV1;
|
||||||
|
case r'UsersV1': return SyncRequestType.usersV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncStackDeleteV1 {
|
||||||
|
/// Returns a new [SyncStackDeleteV1] instance.
|
||||||
|
SyncStackDeleteV1({
|
||||||
|
required this.stackId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String stackId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncStackDeleteV1 &&
|
||||||
|
other.stackId == stackId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(stackId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncStackDeleteV1[stackId=$stackId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'stackId'] = this.stackId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncStackDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncStackDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncStackDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncStackDeleteV1(
|
||||||
|
stackId: mapValueOfType<String>(json, r'stackId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncStackDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncStackDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncStackDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncStackDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncStackDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncStackDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncStackDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncStackDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncStackDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncStackDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'stackId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncStackV1 {
|
||||||
|
/// Returns a new [SyncStackV1] instance.
|
||||||
|
SyncStackV1({
|
||||||
|
required this.createdAt,
|
||||||
|
required this.id,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.primaryAssetId,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
String primaryAssetId;
|
||||||
|
|
||||||
|
DateTime updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncStackV1 &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.id == id &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.primaryAssetId == primaryAssetId &&
|
||||||
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(primaryAssetId.hashCode) +
|
||||||
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncStackV1[createdAt=$createdAt, id=$id, ownerId=$ownerId, primaryAssetId=$primaryAssetId, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
json[r'primaryAssetId'] = this.primaryAssetId;
|
||||||
|
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncStackV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncStackV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncStackV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncStackV1(
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
primaryAssetId: mapValueOfType<String>(json, r'primaryAssetId')!,
|
||||||
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncStackV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncStackV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncStackV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncStackV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncStackV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncStackV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncStackV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncStackV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncStackV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncStackV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'createdAt',
|
||||||
|
'id',
|
||||||
|
'ownerId',
|
||||||
|
'primaryAssetId',
|
||||||
|
'updatedAt',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -28,11 +28,11 @@ function dart {
|
|||||||
|
|
||||||
function typescript {
|
function typescript {
|
||||||
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
|
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
|
||||||
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
|
pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
|
||||||
}
|
}
|
||||||
|
|
||||||
# requires server to be built
|
# requires server to be built
|
||||||
npm run sync:open-api --prefix=../server
|
(cd .. && pnpm --filter immich install && pnpm --filter immich build && pnpm --filter immich sync:open-api)
|
||||||
|
|
||||||
if [[ $1 == 'dart' ]]; then
|
if [[ $1 == 'dart' ]]; then
|
||||||
dart
|
dart
|
||||||
|
|||||||
@@ -13615,36 +13615,41 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"fNumber": {
|
"fNumber": {
|
||||||
|
"format": "double",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"fileSizeInByte": {
|
"fileSizeInByte": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"focalLength": {
|
"focalLength": {
|
||||||
|
"format": "double",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"fps": {
|
"fps": {
|
||||||
|
"format": "double",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"iso": {
|
"iso": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"latitude": {
|
"latitude": {
|
||||||
|
"format": "double",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"lensModel": {
|
"lensModel": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"longitude": {
|
"longitude": {
|
||||||
|
"format": "double",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"make": {
|
"make": {
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
@@ -13804,6 +13809,9 @@
|
|||||||
"PartnerAssetDeleteV1",
|
"PartnerAssetDeleteV1",
|
||||||
"PartnerAssetExifV1",
|
"PartnerAssetExifV1",
|
||||||
"PartnerAssetExifBackfillV1",
|
"PartnerAssetExifBackfillV1",
|
||||||
|
"PartnerStackBackfillV1",
|
||||||
|
"PartnerStackDeleteV1",
|
||||||
|
"PartnerStackV1",
|
||||||
"AlbumV1",
|
"AlbumV1",
|
||||||
"AlbumDeleteV1",
|
"AlbumDeleteV1",
|
||||||
"AlbumUserV1",
|
"AlbumUserV1",
|
||||||
@@ -13820,6 +13828,8 @@
|
|||||||
"MemoryDeleteV1",
|
"MemoryDeleteV1",
|
||||||
"MemoryToAssetV1",
|
"MemoryToAssetV1",
|
||||||
"MemoryToAssetDeleteV1",
|
"MemoryToAssetDeleteV1",
|
||||||
|
"StackV1",
|
||||||
|
"StackDeleteV1",
|
||||||
"SyncAckV1"
|
"SyncAckV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -13971,22 +13981,64 @@
|
|||||||
},
|
},
|
||||||
"SyncRequestType": {
|
"SyncRequestType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"UsersV1",
|
|
||||||
"PartnersV1",
|
|
||||||
"AssetsV1",
|
|
||||||
"AssetExifsV1",
|
|
||||||
"PartnerAssetsV1",
|
|
||||||
"PartnerAssetExifsV1",
|
|
||||||
"AlbumsV1",
|
"AlbumsV1",
|
||||||
"AlbumUsersV1",
|
"AlbumUsersV1",
|
||||||
"AlbumToAssetsV1",
|
"AlbumToAssetsV1",
|
||||||
"AlbumAssetsV1",
|
"AlbumAssetsV1",
|
||||||
"AlbumAssetExifsV1",
|
"AlbumAssetExifsV1",
|
||||||
|
"AssetsV1",
|
||||||
|
"AssetExifsV1",
|
||||||
"MemoriesV1",
|
"MemoriesV1",
|
||||||
"MemoryToAssetsV1"
|
"MemoryToAssetsV1",
|
||||||
|
"PartnersV1",
|
||||||
|
"PartnerAssetsV1",
|
||||||
|
"PartnerAssetExifsV1",
|
||||||
|
"PartnerStacksV1",
|
||||||
|
"StacksV1",
|
||||||
|
"UsersV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"SyncStackDeleteV1": {
|
||||||
|
"properties": {
|
||||||
|
"stackId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stackId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncStackV1": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"primaryAssetId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"id",
|
||||||
|
"ownerId",
|
||||||
|
"primaryAssetId",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncStreamDto": {
|
"SyncStreamDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"types": {
|
"types": {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ class {{{classname}}} {
|
|||||||
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
|
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
|
||||||
{{/isNumber}}
|
{{/isNumber}}
|
||||||
{{#isDouble}}
|
{{#isDouble}}
|
||||||
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
|
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(),
|
||||||
{{/isDouble}}
|
{{/isDouble}}
|
||||||
{{^isDouble}}
|
{{^isDouble}}
|
||||||
{{^isNumber}}
|
{{^isNumber}}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user