Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 782a089da0 | |||
| d5eff115e8 | |||
| a87c2e82cd | |||
| a11ab4c3f7 | |||
| ebf2f9fd7b | |||
| 683af67344 | |||
| d149d6fa3f | |||
| 8c5269c002 | |||
| cf91d9bdfc | |||
| 5579554532 | |||
| 7e35e6985e | |||
| 56756baea2 | |||
| d5923241b5 | |||
| cc471806fe | |||
| 4ce9bce414 | |||
| f8ab533acb | |||
| 2f5d75ce21 |
@@ -74,7 +74,7 @@ install_dependencies() {
|
|||||||
(
|
(
|
||||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||||
export CI=1 FROZEN=1 OFFLINE=1
|
export CI=1 FROZEN=1 OFFLINE=1
|
||||||
run_cmd make setup-web-dev setup-server-dev
|
run_cmd make setup-dev
|
||||||
)
|
)
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ cd "${IMMICH_WORKSPACE}/server" || (
|
|||||||
exit 1
|
exit 1
|
||||||
)
|
)
|
||||||
|
|
||||||
CI=1 pnpm install
|
|
||||||
while true; do
|
while true; do
|
||||||
run_cmd pnpm exec nest start --debug "0.0.0.0:9230" --watch
|
run_cmd node ./node_modules/.bin/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 pnpm exec vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
run_cmd node ./node_modules/.bin/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
|
||||||
|
|||||||
+17
-17
@@ -1,41 +1,41 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
.github/
|
.github/
|
||||||
.git/
|
.git/
|
||||||
.env*
|
|
||||||
*.log
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
**/Dockerfile
|
|
||||||
**/node_modules/
|
|
||||||
**/.pnpm-store/
|
|
||||||
**/dist/
|
|
||||||
**/coverage/
|
|
||||||
**/build/
|
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
|
Dockerfile
|
||||||
!docker/scripts
|
!docker/scripts
|
||||||
|
|
||||||
docs/
|
docs/
|
||||||
!docs/package.json
|
!docs/package.json
|
||||||
!docs/package-lock.json
|
!docs/package-lock.json
|
||||||
|
|
||||||
e2e/
|
e2e/
|
||||||
!e2e/package.json
|
!e2e/package.json
|
||||||
!e2e/package-lock.json
|
!e2e/package-lock.json
|
||||||
|
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
open-api/typescript-sdk/build/
|
cli/coverage/
|
||||||
!open-api/typescript-sdk/package.json
|
cli/dist/
|
||||||
!open-api/typescript-sdk/package-lock.json
|
cli/node_modules/
|
||||||
|
cli/Dockerfile
|
||||||
|
|
||||||
|
open-api/typescript-sdk/build/
|
||||||
|
open-api/typescript-sdk/node_modules/
|
||||||
|
|
||||||
|
server/coverage/
|
||||||
|
server/node_modules/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/src/queries
|
server/src/queries
|
||||||
|
server/dist/
|
||||||
server/www/
|
server/www/
|
||||||
|
server/Dockerfile
|
||||||
|
|
||||||
|
web/node_modules/
|
||||||
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
|
web/build/
|
||||||
|
web/.env
|
||||||
|
web/Dockerfile
|
||||||
|
|||||||
Generated
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": ".github",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.5.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-15
@@ -33,24 +33,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup pnpm
|
# Setup .npmrc file to publish to npm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Prepare SDK
|
||||||
run: pnpm install && pnpm run build
|
run: npm ci --prefix ../open-api/typescript-sdk/
|
||||||
working-directory: ./open-api/typescript-sdk
|
- name: Build SDK
|
||||||
|
run: npm run build --prefix ../open-api/typescript-sdk/
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: npm ci
|
||||||
- run: pnpm build
|
- run: npm run build
|
||||||
- run: pnpm publish
|
- run: npm 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,24 +53,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: './cli/.nvmrc'
|
node-version-file: './docs/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run install
|
- name: Run npm install
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: pnpm build
|
run: npm run 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: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Fix formatting
|
- name: Fix formatting
|
||||||
run: make install-all && make format-all
|
run: make install-all && make format-all
|
||||||
|
|||||||
@@ -20,21 +20,18 @@ 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: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: npm run build
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: pnpm publish
|
run: npm publish
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./mobile
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@@ -56,27 +59,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: dart pub get
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Install DCM
|
- name: Install DCM
|
||||||
run: |
|
# TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
|
||||||
sudo apt-get update
|
uses: bo0tzz/setup-dcm@b4952ab813659c03513b57bd78bfe3f634171f8a
|
||||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
with:
|
||||||
echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
sudo apt-get update
|
version: auto
|
||||||
sudo apt-get install dcm
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: make translation
|
run: make translation
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Run Build Runner
|
- name: Run Build Runner
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Generate platform API
|
- name: Generate platform API
|
||||||
run: make pigeon
|
run: make pigeon
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- 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
|
||||||
@@ -98,19 +97,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Run dart analyze
|
- name: Run dart analyze
|
||||||
run: dart analyze --fatal-infos
|
run: dart analyze --fatal-infos
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Run dart format
|
- name: Run dart format
|
||||||
run: dart format lib/ --set-exit-if-changed
|
run: dart format lib/ --set-exit-if-changed
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Run dart custom_lint
|
- name: Run dart custom_lint
|
||||||
run: dart run custom_lint
|
run: dart run custom_lint
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
|
# TODO: Use https://github.com/CQLabs/dcm-action
|
||||||
- name: Run DCM
|
- name: Run DCM
|
||||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
zizmor:
|
zizmor:
|
||||||
name: zizmor
|
name: zizmor
|
||||||
|
|||||||
+85
-129
@@ -80,33 +80,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: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run package manager install
|
- name: Run npm install
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run small tests & coverage
|
- name: Run small tests & coverage
|
||||||
run: pnpm test
|
run: npm test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
@@ -126,37 +123,34 @@ 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: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: pnpm install && pnpm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests-win:
|
cli-unit-tests-win:
|
||||||
@@ -176,30 +170,27 @@ 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: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
# Skip linter & formatter in Windows test.
|
# Skip linter & formatter in Windows test.
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-lint:
|
web-lint:
|
||||||
@@ -219,33 +210,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: './server/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: pnpm rebuild && pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint:p
|
run: npm run lint:p
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run svelte checks
|
- name: Run svelte checks
|
||||||
run: pnpm check:svelte
|
run: npm run check:svelte
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
@@ -265,29 +253,26 @@ 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: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check:typescript
|
run: npm run check:typescript
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
i18n-tests:
|
i18n-tests:
|
||||||
@@ -303,21 +288,18 @@ 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: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm --filter=immich-web install --frozen-lockfile
|
run: npm --prefix=web ci
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: pnpm --filter=immich-web format:i18n
|
run: npm --prefix=web run 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
|
||||||
@@ -352,35 +334,32 @@ 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: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: pnpm check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
server-medium-tests:
|
server-medium-tests:
|
||||||
@@ -400,21 +379,18 @@ 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: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
run: pnpm test:medium
|
run: npm run test:medium
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-server-cli:
|
e2e-tests-server-cli:
|
||||||
@@ -438,33 +414,25 @@ 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: './server/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run 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: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
@@ -472,7 +440,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: pnpm test
|
run: npm run test
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-web:
|
e2e-tests-web:
|
||||||
@@ -496,23 +464,20 @@ 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: './server/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
@@ -619,21 +584,18 @@ 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: './.github/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
run: pnpm format
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
shellcheck:
|
shellcheck:
|
||||||
@@ -665,21 +627,18 @@ 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: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: pnpm --filter immich install --frozen-lockfile
|
run: npm --prefix=server ci
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm --filter immich build
|
run: npm --prefix=server run build
|
||||||
|
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
@@ -731,31 +690,28 @@ 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: 'pnpm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: npm ci
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: pnpm migrations:run
|
run: npm run migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: pnpm schema:reset
|
run: npm run schema:reset
|
||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: pnpm migrations:generate src/TestMigration
|
run: npm run 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
|
||||||
@@ -774,7 +730,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
run: pnpm sync:sql
|
run: npm run sync:sql
|
||||||
env:
|
env:
|
||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
|
|||||||
@@ -24,4 +24,3 @@ mobile/android/fastlane/report.xml
|
|||||||
mobile/ios/fastlane/report.xml
|
mobile/ios/fastlane/report.xml
|
||||||
|
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
.pnpm-store
|
|
||||||
|
|||||||
@@ -40,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:
|
||||||
pnpm --filter immich run sync:sql
|
npm --prefix server run sync:sql
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
@@ -50,40 +50,31 @@ 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-%:
|
||||||
pnpm --filter $(call map-package,$*) audit fix
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
||||||
install-%:
|
install-%:
|
||||||
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
|
||||||
|
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-%
|
||||||
pnpm --filter $(call map-package,$*) run build
|
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
|
||||||
format-%:
|
format-%:
|
||||||
pnpm --filter $(call map-package,$*) run format:fix
|
npm --prefix $* run format:fix
|
||||||
lint-%:
|
lint-%:
|
||||||
pnpm --filter $(call map-package,$*) run lint:fix
|
npm --prefix $* run lint:fix
|
||||||
lint-web:
|
|
||||||
pnpm --filter $(call map-package,$*) run lint:p
|
|
||||||
check-%:
|
check-%:
|
||||||
pnpm --filter $(call map-package,$*) run check
|
npm --prefix $* run check
|
||||||
check-web:
|
check-web:
|
||||||
pnpm --filter immich-web run check:typescript
|
npm --prefix web run check:typescript
|
||||||
pnpm --filter immich-web run check:svelte
|
npm --prefix web run check:svelte
|
||||||
test-%:
|
test-%:
|
||||||
pnpm --filter $(call map-package,$*) run test
|
npm --prefix $* run test
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
docker compose -f ./e2e/docker-compose.yml build
|
||||||
pnpm --filter immich-e2e run test
|
npm --prefix e2e run test
|
||||||
pnpm --filter immich-e2e run test:web
|
npm --prefix e2e run test:web
|
||||||
test-medium:
|
test-medium:
|
||||||
docker run \
|
docker run \
|
||||||
--rm \
|
--rm \
|
||||||
@@ -93,28 +84,19 @@ 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 "pnpm test:medium -- --run"
|
-c "npm ci && npm run test:medium -- --run"
|
||||||
test-medium-dev:
|
test-medium-dev:
|
||||||
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
|
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||||
|
|
||||||
install-all:
|
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||||
pnpm -r --filter '!documentation' install
|
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||||
|
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||||
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
|
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||||
|
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||||
check-all:
|
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
||||||
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
|
audit-all: $(foreach M,$(MODULES),audit-$M) ;
|
||||||
lint-all:
|
hygiene-all: lint-all format-all check-all sql audit-all;
|
||||||
pnpm -r --filter '!documentation' run lint:fix
|
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
|
||||||
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 {} +
|
||||||
@@ -124,5 +106,4 @@ clean:
|
|||||||
command -v docker >/dev/null 2>&1 && 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
|
||||||
command -v docker >/dev/null 2>&1 && 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-server-dev: install-server
|
setup-dev: install-server install-sdk build-sdk install-web
|
||||||
setup-web-dev: install-sdk build-sdk install-web
|
|
||||||
+4
-8
@@ -6,10 +6,8 @@ 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:
|
||||||
|
|
||||||
# if you don't have node installed
|
$ npm install
|
||||||
$ npm install -g pnpm
|
$ npm run build
|
||||||
$ 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:
|
||||||
|
|
||||||
@@ -17,10 +15,8 @@ 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:
|
||||||
|
|
||||||
# if you don't have node installed
|
$ npm install
|
||||||
$ npm install -g pnpm
|
$ npm run build
|
||||||
$ 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:
|
||||||
|
|||||||
Generated
+4617
File diff suppressed because it is too large
Load Diff
@@ -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/server/bin/immich-dev']
|
command: ['/usr/src/app/bin/immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -27,18 +27,14 @@ services:
|
|||||||
target: dev
|
target: dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ../Makefile:/usr/src/app/Makefile
|
- ../server:/usr/src/app
|
||||||
- ../package.json:/usr/src/app/package.json
|
- ../open-api:/usr/src/open-api
|
||||||
- ../pnpm-lock.yaml:/usr/src/app/pnpm-lock.yaml
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
- ../pnpm-workspace.yaml:/usr/src/app/pnpm-workspace.yaml
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||||
- ../server:/usr/src/app/server
|
- /usr/src/app/node_modules
|
||||||
- ../open-api:/usr/src/app/open-api
|
|
||||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/server/upload
|
|
||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/server/upload/upload
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
# user: ${UID:-1000}:${GID:-1000}
|
|
||||||
environment:
|
environment:
|
||||||
IMMICH_REPOSITORY: immich-app/immich
|
IMMICH_REPOSITORY: immich-app/immich
|
||||||
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
|
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
|
||||||
@@ -72,25 +68,20 @@ services:
|
|||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
# 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
|
||||||
# user: ${UID:-1000}:${GID:-1000}
|
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../web
|
||||||
dockerfile: web/Dockerfile
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
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:
|
||||||
- ../Makefile:/usr/src/app/Makefile
|
- ../web:/usr/src/app
|
||||||
- ../package.json:/usr/src/app/package.json
|
- ../i18n:/usr/src/i18n
|
||||||
- ../pnpm-lock.yaml:/usr/src/app/pnpm-lock.yaml
|
- ../open-api/:/usr/src/open-api/
|
||||||
- ../pnpm-workspace.yaml:/usr/src/app/pnpm-workspace.yaml
|
|
||||||
- ../web:/usr/src/app/web
|
|
||||||
- ../i18n:/usr/src/app/i18n
|
|
||||||
- ../open-api:/usr/src/app/open-api
|
|
||||||
# - ../../ui:/usr/ui
|
# - ../../ui:/usr/ui
|
||||||
|
- /usr/src/app/node_modules
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
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
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pnpm install
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
|
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
||||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ 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: `pnpm install` in all packages
|
- Installs dependencies: `npm 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:
|
||||||
|
|||||||
@@ -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 - `pnpm i`
|
3. Install web dependencies - `npm 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 `pnpm install` (in `server/`) before _once_.
|
You need to run `npm install` (in `server/`) before _once_.
|
||||||
|
|
||||||
### End to end tests
|
### End to end tests
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
|||||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||||
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||||
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||||
| `PNG` | `.png` | :white_check_mark: | |
|
| `PNG` | `.png` | :white_check_mark: | |
|
||||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||||
|
|||||||
Generated
+20545
File diff suppressed because it is too large
Load Diff
Generated
+7409
File diff suppressed because it is too large
Load Diff
@@ -227,6 +227,21 @@ describe(`/oauth`, () => {
|
|||||||
expect(user.storageLabel).toBe('user-username');
|
expect(user.storageLabel).toBe('user-username');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set the admin status from a role claim', async () => {
|
||||||
|
const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE);
|
||||||
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
accessToken: expect.any(String),
|
||||||
|
userId: expect.any(String),
|
||||||
|
userEmail: 'oauth-with-role@immich.app',
|
||||||
|
isAdmin: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
|
||||||
|
expect(user.isAdmin).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with RS256 signed tokens', async () => {
|
it('should work with RS256 signed tokens', async () => {
|
||||||
await setupOAuth(admin.accessToken, {
|
await setupOAuth(admin.accessToken, {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export enum OAuthUser {
|
|||||||
NO_NAME = 'no-name',
|
NO_NAME = 'no-name',
|
||||||
WITH_QUOTA = 'with-quota',
|
WITH_QUOTA = 'with-quota',
|
||||||
WITH_USERNAME = 'with-username',
|
WITH_USERNAME = 'with-username',
|
||||||
|
WITH_ROLE = 'with-role',
|
||||||
}
|
}
|
||||||
|
|
||||||
const claims = [
|
const claims = [
|
||||||
@@ -34,6 +35,12 @@ const claims = [
|
|||||||
preferred_username: 'user-quota',
|
preferred_username: 'user-quota',
|
||||||
immich_quota: 25,
|
immich_quota: 25,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sub: OAuthUser.WITH_ROLE,
|
||||||
|
email: 'oauth-with-role@immich.app',
|
||||||
|
email_verified: true,
|
||||||
|
immich_role: 'admin',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const withDefaultClaims = (sub: string) => ({
|
const withDefaultClaims = (sub: string) => ({
|
||||||
@@ -64,7 +71,15 @@ const setup = async () => {
|
|||||||
claims: {
|
claims: {
|
||||||
openid: ['sub'],
|
openid: ['sub'],
|
||||||
email: ['email', 'email_verified'],
|
email: ['email', 'email_verified'],
|
||||||
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
|
profile: [
|
||||||
|
'name',
|
||||||
|
'given_name',
|
||||||
|
'family_name',
|
||||||
|
'preferred_username',
|
||||||
|
'immich_quota',
|
||||||
|
'immich_username',
|
||||||
|
'immich_role',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
jwtUserinfo: {
|
jwtUserinfo: {
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ 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('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).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 = ["'", '"', ',', '{', '}', '*'];
|
||||||
|
|||||||
@@ -196,6 +196,8 @@
|
|||||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||||
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
||||||
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
||||||
|
"oauth_role_claim": "Role Claim",
|
||||||
|
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
||||||
"oauth_settings": "OAuth",
|
"oauth_settings": "OAuth",
|
||||||
"oauth_settings_description": "Manage OAuth login settings",
|
"oauth_settings_description": "Manage OAuth login settings",
|
||||||
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
||||||
|
|||||||
+1
-1331
File diff suppressed because one or more lines are too long
@@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
// Model for a stack stored in the server
|
||||||
|
class Stack {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final String primaryAssetId;
|
||||||
|
|
||||||
|
const Stack({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.primaryAssetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Stack copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? ownerId,
|
||||||
|
String? primaryAssetId,
|
||||||
|
}) {
|
||||||
|
return Stack(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||||
|
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
||||||
|
'ownerId': ownerId,
|
||||||
|
'primaryAssetId': primaryAssetId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Stack.fromMap(Map<String, dynamic> map) {
|
||||||
|
return Stack(
|
||||||
|
id: map['id'] as String,
|
||||||
|
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
|
||||||
|
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
|
||||||
|
ownerId: map['ownerId'] as String,
|
||||||
|
primaryAssetId: map['primaryAssetId'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory Stack.fromJson(String source) =>
|
||||||
|
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant Stack other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.updatedAt == updatedAt &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.primaryAssetId == primaryAssetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
updatedAt.hashCode ^
|
||||||
|
ownerId.hashCode ^
|
||||||
|
primaryAssetId.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -154,6 +154,25 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
|
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
|
||||||
case SyncEntityType.memoryToAssetDeleteV1:
|
case SyncEntityType.memoryToAssetDeleteV1:
|
||||||
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
|
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.stackV1:
|
||||||
|
return _syncStreamRepository.updateStacksV1(data.cast());
|
||||||
|
case SyncEntityType.stackDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteStacksV1(data.cast());
|
||||||
|
case SyncEntityType.partnerStackV1:
|
||||||
|
return _syncStreamRepository.updateStacksV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner',
|
||||||
|
);
|
||||||
|
case SyncEntityType.partnerStackBackfillV1:
|
||||||
|
return _syncStreamRepository.updateStacksV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.partnerStackDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteStacksV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner',
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
_logger.warning("Unknown sync data type: $type");
|
_logger.warning("Unknown sync data type: $type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,43 @@ class TimelineFactory {
|
|||||||
bucketSource: () =>
|
bucketSource: () =>
|
||||||
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
|
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TimelineService favorite(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getFavoriteBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService trash(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getTrashBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService archive(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getArchiveBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService lockedFolder(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
|
||||||
|
userId,
|
||||||
|
groupBy: groupBy,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService video(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getVideoBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchVideoBucket(userId, groupBy: groupBy),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
|
|||||||
@@ -9,8 +9,57 @@ class BackgroundSyncManager {
|
|||||||
Cancelable<void>? _deviceAlbumSyncTask;
|
Cancelable<void>? _deviceAlbumSyncTask;
|
||||||
Cancelable<void>? _hashTask;
|
Cancelable<void>? _hashTask;
|
||||||
|
|
||||||
|
Completer<void>? _localSyncMutex;
|
||||||
|
Completer<void>? _remoteSyncMutex;
|
||||||
|
Completer<void>? _hashMutex;
|
||||||
|
|
||||||
BackgroundSyncManager();
|
BackgroundSyncManager();
|
||||||
|
|
||||||
|
Future<T> _withMutex<T>(
|
||||||
|
Completer<void>? Function() getMutex,
|
||||||
|
void Function(Completer<void>?) setMutex,
|
||||||
|
Future<T> Function() operation,
|
||||||
|
) async {
|
||||||
|
while (getMutex() != null) {
|
||||||
|
await getMutex()!.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mutex = Completer<void>();
|
||||||
|
setMutex(mutex);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await operation();
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
setMutex(null);
|
||||||
|
mutex.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _withLocalSyncMutex<T>(Future<T> Function() operation) {
|
||||||
|
return _withMutex(
|
||||||
|
() => _localSyncMutex,
|
||||||
|
(mutex) => _localSyncMutex = mutex,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _withRemoteSyncMutex<T>(Future<T> Function() operation) {
|
||||||
|
return _withMutex(
|
||||||
|
() => _remoteSyncMutex,
|
||||||
|
(mutex) => _remoteSyncMutex = mutex,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _withHashMutex<T>(Future<T> Function() operation) {
|
||||||
|
return _withMutex(
|
||||||
|
() => _hashMutex,
|
||||||
|
(mutex) => _hashMutex = mutex,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cancel() {
|
Future<void> cancel() {
|
||||||
final futures = <Future>[];
|
final futures = <Future>[];
|
||||||
|
|
||||||
@@ -25,51 +74,57 @@ class BackgroundSyncManager {
|
|||||||
|
|
||||||
// No need to cancel the task, as it can also be run when the user logs out
|
// No need to cancel the task, as it can also be run when the user logs out
|
||||||
Future<void> syncLocal({bool full = false}) {
|
Future<void> syncLocal({bool full = false}) {
|
||||||
if (_deviceAlbumSyncTask != null) {
|
return _withLocalSyncMutex(() async {
|
||||||
return _deviceAlbumSyncTask!.future;
|
if (_deviceAlbumSyncTask != null) {
|
||||||
}
|
return _deviceAlbumSyncTask!.future;
|
||||||
|
}
|
||||||
|
|
||||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||||
// captured by the closure passed to [runInIsolateGentle].
|
// captured by the closure passed to [runInIsolateGentle].
|
||||||
_deviceAlbumSyncTask = full
|
_deviceAlbumSyncTask = full
|
||||||
? runInIsolateGentle(
|
? runInIsolateGentle(
|
||||||
computation: (ref) =>
|
computation: (ref) =>
|
||||||
ref.read(localSyncServiceProvider).sync(full: true),
|
ref.read(localSyncServiceProvider).sync(full: true),
|
||||||
)
|
)
|
||||||
: runInIsolateGentle(
|
: runInIsolateGentle(
|
||||||
computation: (ref) =>
|
computation: (ref) =>
|
||||||
ref.read(localSyncServiceProvider).sync(full: false),
|
ref.read(localSyncServiceProvider).sync(full: false),
|
||||||
);
|
);
|
||||||
|
|
||||||
return _deviceAlbumSyncTask!.whenComplete(() {
|
return _deviceAlbumSyncTask!.whenComplete(() {
|
||||||
_deviceAlbumSyncTask = null;
|
_deviceAlbumSyncTask = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to cancel the task, as it can also be run when the user logs out
|
// No need to cancel the task, as it can also be run when the user logs out
|
||||||
Future<void> hashAssets() {
|
Future<void> hashAssets() {
|
||||||
if (_hashTask != null) {
|
return _withHashMutex(() async {
|
||||||
return _hashTask!.future;
|
if (_hashTask != null) {
|
||||||
}
|
return _hashTask!.future;
|
||||||
|
}
|
||||||
|
|
||||||
_hashTask = runInIsolateGentle(
|
_hashTask = runInIsolateGentle(
|
||||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||||
);
|
);
|
||||||
return _hashTask!.whenComplete(() {
|
return _hashTask!.whenComplete(() {
|
||||||
_hashTask = null;
|
_hashTask = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncRemote() {
|
Future<void> syncRemote() {
|
||||||
if (_syncTask != null) {
|
return _withRemoteSyncMutex(() async {
|
||||||
return _syncTask!.future;
|
if (_syncTask != null) {
|
||||||
}
|
return _syncTask!.future;
|
||||||
|
}
|
||||||
|
|
||||||
_syncTask = runInIsolateGentle(
|
_syncTask = runInIsolateGentle(
|
||||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||||
);
|
);
|
||||||
return _syncTask!.whenComplete(() {
|
return _syncTask!.whenComplete(() {
|
||||||
_syncTask = null;
|
_syncTask = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class StackEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const StackEntity();
|
||||||
|
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
TextColumn get ownerId =>
|
||||||
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
@@ -0,0 +1,706 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||||
|
as i1;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart' as i2;
|
||||||
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
|
as i4;
|
||||||
|
import 'package:drift/internal/modular.dart' as i5;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
|
as i6;
|
||||||
|
|
||||||
|
typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion
|
||||||
|
Function({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
required String ownerId,
|
||||||
|
required String primaryAssetId,
|
||||||
|
});
|
||||||
|
typedef $$StackEntityTableUpdateCompanionBuilder = i1.StackEntityCompanion
|
||||||
|
Function({
|
||||||
|
i0.Value<String> id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
i0.Value<String> ownerId,
|
||||||
|
i0.Value<String> primaryAssetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final class $$StackEntityTableReferences extends i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase, i1.$StackEntityTable, i1.StackEntityData> {
|
||||||
|
$$StackEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
|
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$StackEntityTable>('stack_entity')
|
||||||
|
.ownerId,
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableProcessedTableManager get ownerId {
|
||||||
|
final $_column = $_itemColumn<String>('owner_id')!;
|
||||||
|
|
||||||
|
final manager = i4
|
||||||
|
.$$UserEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i5.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
static i6.$RemoteAssetEntityTable _primaryAssetIdTable(
|
||||||
|
i0.GeneratedDatabase db) =>
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$StackEntityTable>('stack_entity')
|
||||||
|
.primaryAssetId,
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId {
|
||||||
|
final $_column = $_itemColumn<String>('primary_asset_id')!;
|
||||||
|
|
||||||
|
final manager = i6
|
||||||
|
.$$RemoteAssetEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i5.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
|
||||||
|
$$StackEntityTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnFilters<String> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||||
|
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableFilterComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId {
|
||||||
|
final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.primaryAssetId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i6.$$RemoteAssetEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
|
||||||
|
$$StackEntityTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnOrderings<String> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||||
|
column: $table.updatedAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId {
|
||||||
|
final i6.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.primaryAssetId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i6.$$RemoteAssetEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>(
|
||||||
|
'remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
|
||||||
|
$$StackEntityTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.GeneratedColumn<String> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId {
|
||||||
|
final i6.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.primaryAssetId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i6.$$RemoteAssetEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>(
|
||||||
|
'remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$StackEntityTable,
|
||||||
|
i1.StackEntityData,
|
||||||
|
i1.$$StackEntityTableFilterComposer,
|
||||||
|
i1.$$StackEntityTableOrderingComposer,
|
||||||
|
i1.$$StackEntityTableAnnotationComposer,
|
||||||
|
$$StackEntityTableCreateCompanionBuilder,
|
||||||
|
$$StackEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||||
|
i1.StackEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> {
|
||||||
|
$$StackEntityTableTableManager(
|
||||||
|
i0.GeneratedDatabase db, i1.$StackEntityTable table)
|
||||||
|
: super(i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$$StackEntityTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
i1.$$StackEntityTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
i1.$$StackEntityTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
i0.Value<String> id = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
i0.Value<String> ownerId = const i0.Value.absent(),
|
||||||
|
i0.Value<String> primaryAssetId = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.StackEntityCompanion(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
primaryAssetId: primaryAssetId,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required String primaryAssetId,
|
||||||
|
}) =>
|
||||||
|
i1.StackEntityCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
primaryAssetId: primaryAssetId,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (
|
||||||
|
e.readTable(table),
|
||||||
|
i1.$$StackEntityTableReferences(db, table, e)
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) {
|
||||||
|
return i0.PrefetchHooks(
|
||||||
|
db: db,
|
||||||
|
explicitlyWatchedTables: [],
|
||||||
|
addJoins: <
|
||||||
|
T extends i0.TableManagerState<
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic>>(state) {
|
||||||
|
if (ownerId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.ownerId,
|
||||||
|
referencedTable:
|
||||||
|
i1.$$StackEntityTableReferences._ownerIdTable(db),
|
||||||
|
referencedColumn:
|
||||||
|
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
if (primaryAssetId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.primaryAssetId,
|
||||||
|
referencedTable: i1.$$StackEntityTableReferences
|
||||||
|
._primaryAssetIdTable(db),
|
||||||
|
referencedColumn: i1.$$StackEntityTableReferences
|
||||||
|
._primaryAssetIdTable(db)
|
||||||
|
.id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
getPrefetchedDataCallback: (items) async {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$StackEntityTable,
|
||||||
|
i1.StackEntityData,
|
||||||
|
i1.$$StackEntityTableFilterComposer,
|
||||||
|
i1.$$StackEntityTableOrderingComposer,
|
||||||
|
i1.$$StackEntityTableAnnotationComposer,
|
||||||
|
$$StackEntityTableCreateCompanionBuilder,
|
||||||
|
$$StackEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||||
|
i1.StackEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>;
|
||||||
|
|
||||||
|
class $StackEntityTable extends i2.StackEntity
|
||||||
|
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$StackEntityTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _createdAtMeta =
|
||||||
|
const i0.VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> createdAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: i3.currentDateAndTime);
|
||||||
|
static const i0.VerificationMeta _updatedAtMeta =
|
||||||
|
const i0.VerificationMeta('updatedAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> updatedAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: i3.currentDateAndTime);
|
||||||
|
static const i0.VerificationMeta _ownerIdMeta =
|
||||||
|
const i0.VerificationMeta('ownerId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
|
||||||
|
'owner_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
static const i0.VerificationMeta _primaryAssetIdMeta =
|
||||||
|
const i0.VerificationMeta('primaryAssetId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> primaryAssetId =
|
||||||
|
i0.GeneratedColumn<String>(
|
||||||
|
'primary_asset_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id)'));
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns =>
|
||||||
|
[id, createdAt, updatedAt, ownerId, primaryAssetId];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'stack_entity';
|
||||||
|
@override
|
||||||
|
i0.VerificationContext validateIntegrity(
|
||||||
|
i0.Insertable<i1.StackEntityData> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = i0.VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_idMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('updated_at')) {
|
||||||
|
context.handle(_updatedAtMeta,
|
||||||
|
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('owner_id')) {
|
||||||
|
context.handle(_ownerIdMeta,
|
||||||
|
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_ownerIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('primary_asset_id')) {
|
||||||
|
context.handle(
|
||||||
|
_primaryAssetIdMeta,
|
||||||
|
primaryAssetId.isAcceptableOrUnknown(
|
||||||
|
data['primary_asset_id']!, _primaryAssetIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_primaryAssetIdMeta);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
i1.StackEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.StackEntityData(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
updatedAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
|
||||||
|
ownerId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
|
||||||
|
primaryAssetId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$StackEntityTable createAlias(String alias) {
|
||||||
|
return $StackEntityTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get withoutRowId => true;
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StackEntityData extends i0.DataClass
|
||||||
|
implements i0.Insertable<i1.StackEntityData> {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final String primaryAssetId;
|
||||||
|
const StackEntityData(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.primaryAssetId});
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
map['id'] = i0.Variable<String>(id);
|
||||||
|
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
||||||
|
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId);
|
||||||
|
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory StackEntityData.fromJson(Map<String, dynamic> json,
|
||||||
|
{i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return StackEntityData(
|
||||||
|
id: serializer.fromJson<String>(json['id']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||||
|
ownerId: serializer.fromJson<String>(json['ownerId']),
|
||||||
|
primaryAssetId: serializer.fromJson<String>(json['primaryAssetId']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<String>(id),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||||
|
'ownerId': serializer.toJson<String>(ownerId),
|
||||||
|
'primaryAssetId': serializer.toJson<String>(primaryAssetId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.StackEntityData copyWith(
|
||||||
|
{String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? ownerId,
|
||||||
|
String? primaryAssetId}) =>
|
||||||
|
i1.StackEntityData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||||
|
);
|
||||||
|
StackEntityData copyWithCompanion(i1.StackEntityCompanion data) {
|
||||||
|
return StackEntityData(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||||
|
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
||||||
|
primaryAssetId: data.primaryAssetId.present
|
||||||
|
? data.primaryAssetId.value
|
||||||
|
: this.primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('StackEntityData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('primaryAssetId: $primaryAssetId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.StackEntityData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.updatedAt == this.updatedAt &&
|
||||||
|
other.ownerId == this.ownerId &&
|
||||||
|
other.primaryAssetId == this.primaryAssetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StackEntityCompanion extends i0.UpdateCompanion<i1.StackEntityData> {
|
||||||
|
final i0.Value<String> id;
|
||||||
|
final i0.Value<DateTime> createdAt;
|
||||||
|
final i0.Value<DateTime> updatedAt;
|
||||||
|
final i0.Value<String> ownerId;
|
||||||
|
final i0.Value<String> primaryAssetId;
|
||||||
|
const StackEntityCompanion({
|
||||||
|
this.id = const i0.Value.absent(),
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
this.ownerId = const i0.Value.absent(),
|
||||||
|
this.primaryAssetId = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
StackEntityCompanion.insert({
|
||||||
|
required String id,
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required String primaryAssetId,
|
||||||
|
}) : id = i0.Value(id),
|
||||||
|
ownerId = i0.Value(ownerId),
|
||||||
|
primaryAssetId = i0.Value(primaryAssetId);
|
||||||
|
static i0.Insertable<i1.StackEntityData> custom({
|
||||||
|
i0.Expression<String>? id,
|
||||||
|
i0.Expression<DateTime>? createdAt,
|
||||||
|
i0.Expression<DateTime>? updatedAt,
|
||||||
|
i0.Expression<String>? ownerId,
|
||||||
|
i0.Expression<String>? primaryAssetId,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
|
if (ownerId != null) 'owner_id': ownerId,
|
||||||
|
if (primaryAssetId != null) 'primary_asset_id': primaryAssetId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.StackEntityCompanion copyWith(
|
||||||
|
{i0.Value<String>? id,
|
||||||
|
i0.Value<DateTime>? createdAt,
|
||||||
|
i0.Value<DateTime>? updatedAt,
|
||||||
|
i0.Value<String>? ownerId,
|
||||||
|
i0.Value<String>? primaryAssetId}) {
|
||||||
|
return i1.StackEntityCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = i0.Variable<String>(id.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (updatedAt.present) {
|
||||||
|
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||||
|
}
|
||||||
|
if (ownerId.present) {
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
||||||
|
}
|
||||||
|
if (primaryAssetId.present) {
|
||||||
|
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('StackEntityCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('primaryAssetId: $primaryAssetId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
@@ -50,6 +51,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||||||
RemoteAlbumUserEntity,
|
RemoteAlbumUserEntity,
|
||||||
MemoryEntity,
|
MemoryEntity,
|
||||||
MemoryAssetEntity,
|
MemoryAssetEntity,
|
||||||
|
StackEntity,
|
||||||
],
|
],
|
||||||
include: {
|
include: {
|
||||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||||
|
|||||||
+18
-5
@@ -27,9 +27,11 @@ import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
|||||||
as i12;
|
as i12;
|
||||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||||
as i13;
|
as i13;
|
||||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||||
as i14;
|
as i14;
|
||||||
import 'package:drift/internal/modular.dart' as i15;
|
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||||
|
as i15;
|
||||||
|
import 'package:drift/internal/modular.dart' as i16;
|
||||||
|
|
||||||
abstract class $Drift extends i0.GeneratedDatabase {
|
abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
$Drift(i0.QueryExecutor e) : super(e);
|
$Drift(i0.QueryExecutor e) : super(e);
|
||||||
@@ -58,8 +60,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
|
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
|
||||||
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
|
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
|
||||||
i13.$MemoryAssetEntityTable(this);
|
i13.$MemoryAssetEntityTable(this);
|
||||||
i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this)
|
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
|
||||||
.accessor<i14.MergedAssetDrift>(i14.MergedAssetDrift.new);
|
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
|
||||||
|
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new);
|
||||||
@override
|
@override
|
||||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||||
@@ -80,7 +83,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
remoteAlbumAssetEntity,
|
remoteAlbumAssetEntity,
|
||||||
remoteAlbumUserEntity,
|
remoteAlbumUserEntity,
|
||||||
memoryEntity,
|
memoryEntity,
|
||||||
memoryAssetEntity
|
memoryAssetEntity,
|
||||||
|
stackEntity
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||||
@@ -205,6 +209,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
|
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
@@ -242,4 +253,6 @@ class $DriftManager {
|
|||||||
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||||
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||||
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||||
|
i14.$$StackEntityTableTableManager get stackEntity =>
|
||||||
|
i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class DriftStackRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftStackRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<Stack>> getAll(String userId) {
|
||||||
|
final query = _db.stackEntity.select()
|
||||||
|
..where((e) => e.ownerId.equals(userId));
|
||||||
|
|
||||||
|
return query.map((stack) {
|
||||||
|
return stack.toDto();
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on StackEntityData {
|
||||||
|
Stack toDto() {
|
||||||
|
return Stack(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
primaryAssetId: primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,8 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.albumToAssetsV1,
|
SyncRequestType.albumToAssetsV1,
|
||||||
SyncRequestType.memoriesV1,
|
SyncRequestType.memoriesV1,
|
||||||
SyncRequestType.memoryToAssetsV1,
|
SyncRequestType.memoryToAssetsV1,
|
||||||
|
SyncRequestType.stacksV1,
|
||||||
|
SyncRequestType.partnerStacksV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
@@ -163,6 +165,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
||||||
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
|
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
|
||||||
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
|
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
|
||||||
|
SyncEntityType.stackV1: SyncStackV1.fromJson,
|
||||||
|
SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||||
|
SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
|
||||||
|
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
|
||||||
|
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
class _SyncAckV1 {
|
class _SyncAckV1 {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -69,8 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
|
_logger.severe('Error: SyncPartnerDeleteV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
|
_logger.severe('Error: SyncPartnerV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,10 +105,10 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAssetEntity.deleteWhere(
|
await _db.remoteAssetEntity.deleteWhere(
|
||||||
(row) => row.id.isIn(data.map((error) => error.assetId)),
|
(row) => row.id.isIn(data.map((e) => e.assetId)),
|
||||||
);
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
|
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
|
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,11 +187,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
'Error: updateAssetsExifV1 - $debugLabel',
|
'Error: updateAssetsExifV1 - $debugLabel',
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stack,
|
||||||
);
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -201,8 +202,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
await _db.remoteAlbumEntity.deleteWhere(
|
await _db.remoteAlbumEntity.deleteWhere(
|
||||||
(row) => row.id.isIn(data.map((e) => e.albumId)),
|
(row) => row.id.isIn(data.map((e) => e.albumId)),
|
||||||
);
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
|
_logger.severe('Error: deleteAlbumsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
|
_logger.severe('Error: updateAlbumsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
|
_logger.severe('Error: deleteAlbumUsersV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,11 +276,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
'Error: updateAlbumUsersV1 - $debugLabel',
|
'Error: updateAlbumUsersV1 - $debugLabel',
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stack,
|
||||||
);
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -300,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
|
_logger.severe('Error: deleteAlbumToAssetsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,11 +326,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
'Error: updateAlbumToAssetsV1 - $debugLabel',
|
'Error: updateAlbumToAssetsV1 - $debugLabel',
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stack,
|
||||||
);
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -359,8 +360,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateMemoriesV1', error, stackTrace);
|
_logger.severe('Error: updateMemoriesV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,8 +371,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
await _db.memoryEntity.deleteWhere(
|
await _db.memoryEntity.deleteWhere(
|
||||||
(row) => row.id.isIn(data.map((e) => e.memoryId)),
|
(row) => row.id.isIn(data.map((e) => e.memoryId)),
|
||||||
);
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteMemoriesV1', error, stackTrace);
|
_logger.severe('Error: deleteMemoriesV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,8 +393,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateMemoryAssetsV1', error, stackTrace);
|
_logger.severe('Error: updateMemoryAssetsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,8 +414,49 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace);
|
_logger.severe('Error: deleteMemoryAssetsV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateStacksV1(
|
||||||
|
Iterable<SyncStackV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final stack in data) {
|
||||||
|
final companion = StackEntityCompanion(
|
||||||
|
createdAt: Value(stack.createdAt),
|
||||||
|
updatedAt: Value(stack.updatedAt),
|
||||||
|
ownerId: Value(stack.ownerId),
|
||||||
|
primaryAssetId: Value(stack.primaryAssetId),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.stackEntity,
|
||||||
|
companion.copyWith(id: Value(stack.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: updateStacksV1 - $debugLabel', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteStacksV1(
|
||||||
|
Iterable<SyncStackDeleteV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _db.stackEntity.deleteWhere(
|
||||||
|
(row) => row.id.isIn(data.map((e) => e.stackId)),
|
||||||
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,7 +509,7 @@ extension on String {
|
|||||||
Duration? toDuration() {
|
Duration? toDuration() {
|
||||||
try {
|
try {
|
||||||
final parts = split(':')
|
final parts = split(':')
|
||||||
.map((error) => double.parse(error).toInt())
|
.map((e) => double.parse(e).toInt())
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
||||||
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||||
|
|||||||
@@ -213,6 +213,262 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchFavoriteBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.isFavorite.equals(true) & row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.isFavorite.equals(true),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getFavoriteBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) => row.isFavorite.equals(true) & row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchTrashBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.deletedAt.isNotNull(),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getTrashBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchArchiveBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.visibility.equalsValue(AssetVisibility.archive) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.archive),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getArchiveBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) =>
|
||||||
|
row.ownerId.equals(userId) &
|
||||||
|
row.visibility.equalsValue(AssetVisibility.archive),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchLockedFolderBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.visibility.equalsValue(AssetVisibility.locked) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.locked),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getLockedFolderBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) =>
|
||||||
|
row.visibility.equalsValue(AssetVisibility.locked) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchVideoBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.type.equalsValue(AssetType.video) &
|
||||||
|
row.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.timeline),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getVideoBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) =>
|
||||||
|
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.timeline) &
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Expression<DateTime> {
|
extension on Expression<DateTime> {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
const MainTimelineRoute(),
|
const MainTimelineRoute(),
|
||||||
SearchRoute(),
|
SearchRoute(),
|
||||||
const DriftAlbumsRoute(),
|
const DriftAlbumsRoute(),
|
||||||
const LibraryRoute(),
|
const DriftLibraryRoute(),
|
||||||
],
|
],
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 600),
|
||||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftArchivePage extends StatelessWidget {
|
||||||
|
const DriftArchivePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access archive');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).archive(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftFavoritePage extends StatelessWidget {
|
||||||
|
const DriftFavoritePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access favorite');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).favorite(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftLockedFolderPage extends StatelessWidget {
|
||||||
|
const DriftLockedFolderPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access locked folder');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).lockedFolder(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftTrashPage extends StatelessWidget {
|
||||||
|
const DriftTrashPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access trash');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).trash(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftVideoPage extends StatelessWidget {
|
||||||
|
const DriftVideoPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to video');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).video(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,9 @@ final _features = [
|
|||||||
await db.remoteAlbumEntity.deleteAll();
|
await db.remoteAlbumEntity.deleteAll();
|
||||||
await db.remoteAlbumUserEntity.deleteAll();
|
await db.remoteAlbumUserEntity.deleteAll();
|
||||||
await db.remoteAlbumAssetEntity.deleteAll();
|
await db.remoteAlbumAssetEntity.deleteAll();
|
||||||
|
await db.memoryEntity.deleteAll();
|
||||||
|
await db.memoryAssetEntity.deleteAll();
|
||||||
|
await db.stackEntity.deleteAll();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
@@ -96,6 +99,11 @@ final _features = [
|
|||||||
icon: Icons.timeline_rounded,
|
icon: Icons.timeline_rounded,
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||||
),
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Video',
|
||||||
|
icon: Icons.video_collection_outlined,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ final _remoteStats = [
|
|||||||
name: 'Memories Assets',
|
name: 'Memories Assets',
|
||||||
load: (db) => db.managers.memoryAssetEntity.count(),
|
load: (db) => db.managers.memoryAssetEntity.count(),
|
||||||
),
|
),
|
||||||
|
_Stat(
|
||||||
|
name: 'Stacks',
|
||||||
|
load: (db) => db.managers.stackEntity.count(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -0,0 +1,501 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftLibraryPage extends ConsumerWidget {
|
||||||
|
const DriftLibraryPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
ImmichSliverAppBar(),
|
||||||
|
_ActionButtonGrid(),
|
||||||
|
_CollectionCards(),
|
||||||
|
_QuickAccessButtonList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionButtonGrid extends ConsumerWidget {
|
||||||
|
const _ActionButtonGrid();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isTrashEnable = ref.watch(
|
||||||
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 12),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ActionButton(
|
||||||
|
icon: Icons.favorite_outline_rounded,
|
||||||
|
onTap: () => context.pushRoute(const DriftFavoriteRoute()),
|
||||||
|
label: 'favorites'.t(context: context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_ActionButton(
|
||||||
|
icon: Icons.archive_outlined,
|
||||||
|
onTap: () => context.pushRoute(const DriftArchiveRoute()),
|
||||||
|
label: 'archived'.t(context: context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ActionButton(
|
||||||
|
icon: Icons.link_outlined,
|
||||||
|
onTap: () => context.pushRoute(const SharedLinkRoute()),
|
||||||
|
label: 'shared_links'.t(context: context),
|
||||||
|
),
|
||||||
|
isTrashEnable
|
||||||
|
? const SizedBox(width: 8)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
isTrashEnable
|
||||||
|
? _ActionButton(
|
||||||
|
icon: Icons.delete_outline_rounded,
|
||||||
|
onTap: () => context.pushRoute(const DriftTrashRoute()),
|
||||||
|
label: 'trash'.t(context: context),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionButton extends StatelessWidget {
|
||||||
|
const _ActionButton({
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: onTap,
|
||||||
|
label: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
icon,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollectionCards extends StatelessWidget {
|
||||||
|
const _CollectionCards();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SliverPadding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
_PeopleCollectionCard(),
|
||||||
|
_PlacesCollectionCard(),
|
||||||
|
_LocalAlbumsCollectionCard(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PeopleCollectionCard extends ConsumerWidget {
|
||||||
|
const _PeopleCollectionCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final isTablet = constraints.maxWidth > 600;
|
||||||
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||||
|
final size = context.width * widthFactor - 20.0;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(30),
|
||||||
|
context.colorScheme.primary.withAlpha(25),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: people.widgetWhen(
|
||||||
|
onLoading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
onData: (people) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: people.take(4).map((person) {
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
getFaceThumbnailUrl(person.id),
|
||||||
|
headers: ApiService.getRequestHeaders(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'people'.t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlacesCollectionCard extends StatelessWidget {
|
||||||
|
const _PlacesCollectionCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final isTablet = constraints.maxWidth > 600;
|
||||||
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||||
|
final size = context.width * widthFactor - 20.0;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
PlacesCollectionRoute(
|
||||||
|
currentLocation: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
color:
|
||||||
|
context.colorScheme.secondaryContainer.withAlpha(100),
|
||||||
|
),
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: MapThumbnail(
|
||||||
|
zoom: 8,
|
||||||
|
centre: const LatLng(
|
||||||
|
21.44950,
|
||||||
|
-157.91959,
|
||||||
|
),
|
||||||
|
showAttribution: false,
|
||||||
|
themeMode: context.isDarkTheme
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'places'.t(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalAlbumsCollectionCard extends ConsumerWidget {
|
||||||
|
const _LocalAlbumsCollectionCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// TODO: Migrate to the drift after local album page
|
||||||
|
final albums = ref.watch(localAlbumsProvider);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final isTablet = constraints.maxWidth > 600;
|
||||||
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||||
|
final size = context.width * widthFactor - 20.0;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
const LocalAlbumsRoute(),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(30),
|
||||||
|
context.colorScheme.primary.withAlpha(25),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: albums.take(4).map((album) {
|
||||||
|
return AlbumThumbnailCard(
|
||||||
|
album: album,
|
||||||
|
showTitle: false,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'on_this_device'.t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickAccessButtonList extends ConsumerWidget {
|
||||||
|
const _QuickAccessButtonList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final partners = ref.watch(partnerSharedWithProvider);
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
context.colorScheme.primary.withAlpha(20),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: const Radius.circular(20),
|
||||||
|
topRight: const Radius.circular(20),
|
||||||
|
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||||
|
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.folder_outlined,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'folders'.t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(FolderRoute()),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'locked_folder'.t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// TODO: PIN code is needed
|
||||||
|
onTap: () => context.pushRoute(const DriftLockedFolderRoute()),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.group_outlined,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'partners'.t(context: context),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||||
|
),
|
||||||
|
_PartnerList(partners: partners),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PartnerList extends StatelessWidget {
|
||||||
|
const _PartnerList({required this.partners});
|
||||||
|
|
||||||
|
final List<UserDto> partners;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: partners.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final partner = partners[index];
|
||||||
|
final isLastItem = index == partners.length - 1;
|
||||||
|
return ListTile(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
|
||||||
|
bottomRight: Radius.circular(isLastItem ? 20 : 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 12.0,
|
||||||
|
right: 18.0,
|
||||||
|
),
|
||||||
|
leading: userAvatar(context, partner, radius: 16),
|
||||||
|
title: const Text(
|
||||||
|
"partner_list_user_photos",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
).t(context: context, args: {'user': partner.name}),
|
||||||
|
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -354,22 +354,24 @@ class ScrubberState extends ConsumerState<Scrubber>
|
|||||||
isDragging: _isDragging,
|
isDragging: _isDragging,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PositionedDirectional(
|
if (_scrollController.hasClients &&
|
||||||
top: _thumbTopOffset + widget.topPadding,
|
_scrollController.position.maxScrollExtent > 0)
|
||||||
end: 0,
|
PositionedDirectional(
|
||||||
child: RepaintBoundary(
|
top: _thumbTopOffset + widget.topPadding,
|
||||||
child: GestureDetector(
|
end: 0,
|
||||||
onVerticalDragStart: _onDragStart,
|
child: RepaintBoundary(
|
||||||
onVerticalDragUpdate: _onDragUpdate,
|
child: GestureDetector(
|
||||||
onVerticalDragEnd: _onDragEnd,
|
onVerticalDragStart: _onDragStart,
|
||||||
child: _Scrubber(
|
onVerticalDragUpdate: _onDragUpdate,
|
||||||
thumbAnimation: _thumbAnimation,
|
onVerticalDragEnd: _onDragEnd,
|
||||||
labelAnimation: _labelAnimation,
|
child: _Scrubber(
|
||||||
label: label,
|
thumbAnimation: _thumbAnimation,
|
||||||
|
labelAnimation: _labelAnimation,
|
||||||
|
label: label,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
|
final driftStackProvider = Provider<DriftStackRepository>(
|
||||||
|
(ref) => DriftStackRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
@@ -40,6 +40,9 @@ class AuthRepository extends DatabaseRepository {
|
|||||||
_drift.remoteAlbumEntity.deleteAll(),
|
_drift.remoteAlbumEntity.deleteAll(),
|
||||||
_drift.remoteAlbumAssetEntity.deleteAll(),
|
_drift.remoteAlbumAssetEntity.deleteAll(),
|
||||||
_drift.remoteAlbumUserEntity.deleteAll(),
|
_drift.remoteAlbumUserEntity.deleteAll(),
|
||||||
|
_drift.memoryEntity.deleteAll(),
|
||||||
|
_drift.memoryAssetEntity.deleteAll(),
|
||||||
|
_drift.stackEntity.deleteAll(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,12 +66,18 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_locked_folder.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
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/presentation/pages/drift_album.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@@ -174,7 +180,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
maintainState: false,
|
maintainState: false,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: LibraryRoute.page,
|
page: DriftLibraryRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
@@ -392,7 +398,30 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: DriftMemoryRoute.page,
|
page: DriftMemoryRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftFavoriteRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftTrashRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftArchiveRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftLockedFolderRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftVideoRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftLibraryRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
|||||||
@@ -618,6 +618,70 @@ class DriftAlbumsRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftArchivePage]
|
||||||
|
class DriftArchiveRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftArchiveRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftArchiveRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftArchiveRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftArchivePage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftFavoritePage]
|
||||||
|
class DriftFavoriteRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftFavoriteRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftFavoriteRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftFavoriteRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftFavoritePage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftLibraryPage]
|
||||||
|
class DriftLibraryRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftLibraryRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftLibraryRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftLibraryRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftLibraryPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftLockedFolderPage]
|
||||||
|
class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftLockedFolderRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftLockedFolderRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftLockedFolderRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftLockedFolderPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftMemoryPage]
|
/// [DriftMemoryPage]
|
||||||
class DriftMemoryRoute extends PageRouteInfo<DriftMemoryRouteArgs> {
|
class DriftMemoryRoute extends PageRouteInfo<DriftMemoryRouteArgs> {
|
||||||
@@ -670,6 +734,38 @@ class DriftMemoryRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftTrashPage]
|
||||||
|
class DriftTrashRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftTrashRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftTrashRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftTrashRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftTrashPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftVideoPage]
|
||||||
|
class DriftVideoRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftVideoRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftVideoRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftVideoRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftVideoPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [EditImagePage]
|
/// [EditImagePage]
|
||||||
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
|
onPressed: () {
|
||||||
|
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
|
ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.sync,
|
Icons.sync,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
bool isDarkTheme = context.themeData.brightness == Brightness.dark;
|
final userAvatarColor = user.avatarColor.toColor();
|
||||||
final profileImageUrl =
|
final profileImageUrl =
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||||
|
|
||||||
@@ -34,14 +32,14 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: isDarkTheme && user.avatarColor == AvatarColor.primary
|
color: userAvatarColor.computeLuminance() > 0.5
|
||||||
? Colors.black
|
? Colors.black
|
||||||
: Colors.white,
|
: Colors.white,
|
||||||
),
|
),
|
||||||
child: Text(user.name[0].toUpperCase()),
|
child: Text(user.name[0].toUpperCase()),
|
||||||
);
|
);
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: user.avatarColor.toColor(),
|
backgroundColor: userAvatarColor,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
child: user.profileImagePath == null
|
child: user.profileImagePath == null
|
||||||
? textIcon
|
? textIcon
|
||||||
|
|||||||
+9
-1
@@ -24,6 +24,7 @@ class SystemConfigOAuthDto {
|
|||||||
required this.mobileOverrideEnabled,
|
required this.mobileOverrideEnabled,
|
||||||
required this.mobileRedirectUri,
|
required this.mobileRedirectUri,
|
||||||
required this.profileSigningAlgorithm,
|
required this.profileSigningAlgorithm,
|
||||||
|
required this.roleClaim,
|
||||||
required this.scope,
|
required this.scope,
|
||||||
required this.signingAlgorithm,
|
required this.signingAlgorithm,
|
||||||
required this.storageLabelClaim,
|
required this.storageLabelClaim,
|
||||||
@@ -55,6 +56,8 @@ class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
String profileSigningAlgorithm;
|
String profileSigningAlgorithm;
|
||||||
|
|
||||||
|
String roleClaim;
|
||||||
|
|
||||||
String scope;
|
String scope;
|
||||||
|
|
||||||
String signingAlgorithm;
|
String signingAlgorithm;
|
||||||
@@ -81,6 +84,7 @@ class SystemConfigOAuthDto {
|
|||||||
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
||||||
other.mobileRedirectUri == mobileRedirectUri &&
|
other.mobileRedirectUri == mobileRedirectUri &&
|
||||||
other.profileSigningAlgorithm == profileSigningAlgorithm &&
|
other.profileSigningAlgorithm == profileSigningAlgorithm &&
|
||||||
|
other.roleClaim == roleClaim &&
|
||||||
other.scope == scope &&
|
other.scope == scope &&
|
||||||
other.signingAlgorithm == signingAlgorithm &&
|
other.signingAlgorithm == signingAlgorithm &&
|
||||||
other.storageLabelClaim == storageLabelClaim &&
|
other.storageLabelClaim == storageLabelClaim &&
|
||||||
@@ -102,6 +106,7 @@ class SystemConfigOAuthDto {
|
|||||||
(mobileOverrideEnabled.hashCode) +
|
(mobileOverrideEnabled.hashCode) +
|
||||||
(mobileRedirectUri.hashCode) +
|
(mobileRedirectUri.hashCode) +
|
||||||
(profileSigningAlgorithm.hashCode) +
|
(profileSigningAlgorithm.hashCode) +
|
||||||
|
(roleClaim.hashCode) +
|
||||||
(scope.hashCode) +
|
(scope.hashCode) +
|
||||||
(signingAlgorithm.hashCode) +
|
(signingAlgorithm.hashCode) +
|
||||||
(storageLabelClaim.hashCode) +
|
(storageLabelClaim.hashCode) +
|
||||||
@@ -110,7 +115,7 @@ class SystemConfigOAuthDto {
|
|||||||
(tokenEndpointAuthMethod.hashCode);
|
(tokenEndpointAuthMethod.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
|
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -129,6 +134,7 @@ class SystemConfigOAuthDto {
|
|||||||
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
||||||
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
|
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
|
||||||
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
|
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
|
||||||
|
json[r'roleClaim'] = this.roleClaim;
|
||||||
json[r'scope'] = this.scope;
|
json[r'scope'] = this.scope;
|
||||||
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
||||||
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
||||||
@@ -158,6 +164,7 @@ class SystemConfigOAuthDto {
|
|||||||
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
||||||
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
|
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
|
||||||
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
|
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
|
||||||
|
roleClaim: mapValueOfType<String>(json, r'roleClaim')!,
|
||||||
scope: mapValueOfType<String>(json, r'scope')!,
|
scope: mapValueOfType<String>(json, r'scope')!,
|
||||||
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
||||||
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
||||||
@@ -222,6 +229,7 @@ class SystemConfigOAuthDto {
|
|||||||
'mobileOverrideEnabled',
|
'mobileOverrideEnabled',
|
||||||
'mobileRedirectUri',
|
'mobileRedirectUri',
|
||||||
'profileSigningAlgorithm',
|
'profileSigningAlgorithm',
|
||||||
|
'roleClaim',
|
||||||
'scope',
|
'scope',
|
||||||
'signingAlgorithm',
|
'signingAlgorithm',
|
||||||
'storageLabelClaim',
|
'storageLabelClaim',
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ void main() {
|
|||||||
.thenAnswer(successHandler);
|
.thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()))
|
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()))
|
||||||
.thenAnswer(successHandler);
|
.thenAnswer(successHandler);
|
||||||
|
when(
|
||||||
|
() => mockSyncStreamRepo.updateStacksV1(
|
||||||
|
any(),
|
||||||
|
debugLabel: any(named: 'debugLabel'),
|
||||||
|
),
|
||||||
|
).thenAnswer(successHandler);
|
||||||
|
when(
|
||||||
|
() => mockSyncStreamRepo.deleteStacksV1(
|
||||||
|
any(),
|
||||||
|
debugLabel: any(named: 'debugLabel'),
|
||||||
|
),
|
||||||
|
).thenAnswer(successHandler);
|
||||||
|
|
||||||
sut = SyncStreamService(
|
sut = SyncStreamService(
|
||||||
syncApiRepository: mockSyncApiRepo,
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
|||||||
@@ -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
|
||||||
pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
|
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
|
||||||
}
|
}
|
||||||
|
|
||||||
# requires server to be built
|
# requires server to be built
|
||||||
(cd .. && pnpm --filter immich install && pnpm --filter immich build && pnpm --filter immich sync:open-api)
|
npm run sync:open-api --prefix=../server
|
||||||
|
|
||||||
if [[ $1 == 'dart' ]]; then
|
if [[ $1 == 'dart' ]]; then
|
||||||
dart
|
dart
|
||||||
|
|||||||
@@ -14654,6 +14654,9 @@
|
|||||||
"profileSigningAlgorithm": {
|
"profileSigningAlgorithm": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"roleClaim": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -14690,6 +14693,7 @@
|
|||||||
"mobileOverrideEnabled",
|
"mobileOverrideEnabled",
|
||||||
"mobileRedirectUri",
|
"mobileRedirectUri",
|
||||||
"profileSigningAlgorithm",
|
"profileSigningAlgorithm",
|
||||||
|
"roleClaim",
|
||||||
"scope",
|
"scope",
|
||||||
"signingAlgorithm",
|
"signingAlgorithm",
|
||||||
"storageLabelClaim",
|
"storageLabelClaim",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ A TypeScript SDK for interfacing with the [Immich](https://immich.app/) API.
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm i --save @immich/sdk
|
npm i --save @immich/sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "@immich/sdk",
|
||||||
|
"version": "1.135.3",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@immich/sdk",
|
||||||
|
"version": "1.135.3",
|
||||||
|
"license": "GNU Affero General Public License version 3",
|
||||||
|
"dependencies": {
|
||||||
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.33",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oazapfts/runtime": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-7t6C2shug/6tZhQgkCa532oTYBLEnbASV/i1SG1rH2GB4h3aQQujYciYSPT92hvN4IwTe8S2hPkN/6iiOyTlCg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.15.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz",
|
||||||
|
"integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,5 @@
|
|||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.0"
|
"node": "22.17.0"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1398,6 +1398,7 @@ export type SystemConfigOAuthDto = {
|
|||||||
mobileOverrideEnabled: boolean;
|
mobileOverrideEnabled: boolean;
|
||||||
mobileRedirectUri: string;
|
mobileRedirectUri: string;
|
||||||
profileSigningAlgorithm: string;
|
profileSigningAlgorithm: string;
|
||||||
|
roleClaim: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
signingAlgorithm: string;
|
signingAlgorithm: string;
|
||||||
storageLabelClaim: string;
|
storageLabelClaim: string;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "immich-monorepo",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "monorepo for immich and friends",
|
|
||||||
"private": true,
|
|
||||||
"packageManager": "pnpm@10.12.4",
|
|
||||||
"engines": {
|
|
||||||
"pnpm": ">=10.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-25306
File diff suppressed because it is too large
Load Diff
@@ -1,79 +0,0 @@
|
|||||||
packages:
|
|
||||||
- cli
|
|
||||||
- docs
|
|
||||||
- e2e
|
|
||||||
- open-api/typescript-sdk
|
|
||||||
- server
|
|
||||||
- web
|
|
||||||
- .github
|
|
||||||
|
|
||||||
|
|
||||||
ignoredBuiltDependencies:
|
|
||||||
- '@nestjs/core'
|
|
||||||
- '@scarf/scarf'
|
|
||||||
- '@swc/core'
|
|
||||||
- bcrypt
|
|
||||||
- canvas
|
|
||||||
- core-js
|
|
||||||
- core-js-pure
|
|
||||||
- cpu-features
|
|
||||||
- es5-ext
|
|
||||||
- esbuild
|
|
||||||
- msgpackr-extract
|
|
||||||
- postman-code-generators
|
|
||||||
- protobufjs
|
|
||||||
- ssh2
|
|
||||||
- utimes
|
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- sharp
|
|
||||||
- '@tailwindcss/oxide'
|
|
||||||
|
|
||||||
overrides:
|
|
||||||
canvas: 2.11.2
|
|
||||||
sharp: ^0.34.2
|
|
||||||
'@img/sharp-darwin-arm64': '-'
|
|
||||||
'@img/sharp-darwin-x64': '-'
|
|
||||||
'@img/sharp-libvips-darwin-arm64': '-'
|
|
||||||
'@img/sharp-libvips-darwin-x64': '-'
|
|
||||||
'@img/sharp-libvips-linux-arm': '-'
|
|
||||||
'@img/sharp-libvips-linux-ppc64': '-'
|
|
||||||
'@img/sharp-libvips-linux-s390x': '-'
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64': '-'
|
|
||||||
'@img/sharp-linux-arm64': '-'
|
|
||||||
'@img/sharp-linux-s390x': '-'
|
|
||||||
'@img/sharp-linuxmusl-arm64': '-'
|
|
||||||
'@img/sharp-wasm32': '-'
|
|
||||||
'@img/sharp-win32-arm64': '-'
|
|
||||||
'@img/sharp-win32-ia32': '-'
|
|
||||||
'@img/sharp-win32-x64': '-'
|
|
||||||
|
|
||||||
packageExtensions:
|
|
||||||
nestjs-kysely:
|
|
||||||
dependencies:
|
|
||||||
tslib: '*'
|
|
||||||
nestjs-otel:
|
|
||||||
dependencies:
|
|
||||||
tslib: '*'
|
|
||||||
'@photo-sphere-viewer/equirectangular-video-adapter':
|
|
||||||
dependencies:
|
|
||||||
three: '*'
|
|
||||||
'@photo-sphere-viewer/video-plugin':
|
|
||||||
dependencies:
|
|
||||||
three: '*'
|
|
||||||
sharp:
|
|
||||||
dependencies:
|
|
||||||
node-addon-api: '*'
|
|
||||||
node-gyp: '*'
|
|
||||||
'@immich/ui':
|
|
||||||
dependencies:
|
|
||||||
tailwindcss: ^4.1.11
|
|
||||||
tailwind-variants:
|
|
||||||
dependencies:
|
|
||||||
tailwindcss: ^4.1.11
|
|
||||||
|
|
||||||
dedupePeerDependents: false
|
|
||||||
packageImportMethod: hardlink
|
|
||||||
preferWorkspacePackages: true
|
|
||||||
shamefullyHoist: false
|
|
||||||
injectWorkspacePackages: true
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
src
|
|
||||||
tsconfig*
|
|
||||||
eslint*
|
|
||||||
pnpm*
|
|
||||||
coverage
|
|
||||||
+75
-99
@@ -1,68 +1,52 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:commit-95312641aeba3ecaab9d73d05ca5d9746ab633b6 AS dev
|
FROM ghcr.io/immich-app/base-server-dev:202505131114@sha256:cf4507bbbf307e9b6d8ee9418993321f2b85867da8ce14d0a20ccaf9574cb995 AS dev
|
||||||
|
|
||||||
ENV COREPACK_ENABLE_AUTO_PIN=0 \
|
|
||||||
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
|
||||||
CI=1
|
|
||||||
|
|
||||||
RUN echo "umask 000" >> /etc/profile && \
|
|
||||||
echo "umask 000" >> /etc/bash.bashrc && \
|
|
||||||
umask 000 && \
|
|
||||||
corepack enable && \
|
|
||||||
corepack install -g pnpm && \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get install --no-install-recommends -yqq tini make && \
|
|
||||||
rm -rf /var/lib/apt/lists && \
|
|
||||||
rm -rf /usr/src/app && \
|
|
||||||
mkdir -p /usr/src/app
|
|
||||||
|
|
||||||
|
RUN apt-get install --no-install-recommends -yqq tini
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./server ./server/
|
COPY server/package.json server/package-lock.json ./
|
||||||
COPY Makefile ./package* ./pnpm* ./
|
RUN npm ci && \
|
||||||
|
# exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
|
||||||
RUN umask 000 && mkdir -p /buildcache/pnpm-store && \
|
# they're marked as optional dependencies, so we need to copy them manually after pruning
|
||||||
pnpm config set store-dir /buildcache/pnpm-store && \
|
rm -rf node_modules/@img/sharp-libvips* && \
|
||||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true make setup-server-dev
|
rm -rf node_modules/@img/sharp-linuxmusl-x64
|
||||||
|
|
||||||
ENV PATH="${PATH}:/usr/src/app/bin" \
|
ENV PATH="${PATH}:/usr/src/app/bin" \
|
||||||
IMMICH_ENV=development \
|
IMMICH_ENV=development \
|
||||||
NVIDIA_DRIVER_CAPABILITIES=all
|
NVIDIA_DRIVER_CAPABILITIES=all \
|
||||||
|
NVIDIA_VISIBLE_DEVICES=all
|
||||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||||
|
|
||||||
FROM dev AS dev-container-server
|
FROM dev AS dev-container-server
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install sudo inetutils-ping openjdk-11-jre-headless \
|
|
||||||
vim nano -y --no-install-recommends --fix-missing && \
|
|
||||||
rm -rf /var/lib/apt/lists
|
|
||||||
RUN usermod -aG sudo node && \
|
|
||||||
echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
|
||||||
# create workspaces dirs
|
|
||||||
mkdir -p /workspaces/immich && \
|
|
||||||
mkdir /immich-devcontainer;
|
|
||||||
|
|
||||||
WORKDIR /workspaces/immich
|
|
||||||
# Remove app dir from dev container
|
|
||||||
RUN rm -rf /usr/src/app
|
RUN rm -rf /usr/src/app
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install sudo inetutils-ping openjdk-11-jre-headless \
|
||||||
|
vim nano \
|
||||||
|
-y --no-install-recommends --fix-missing
|
||||||
|
|
||||||
COPY --chmod=777 ../.devcontainer/server/*.sh /immich-devcontainer/
|
RUN usermod -aG sudo node
|
||||||
|
RUN echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||||
|
RUN mkdir -p /workspaces/immich
|
||||||
|
RUN chown node -R /workspaces
|
||||||
|
COPY --chown=node:node --chmod=777 ../.devcontainer/server/*.sh /immich-devcontainer/
|
||||||
|
|
||||||
|
USER node
|
||||||
|
COPY --chown=node:node .. /tmp/create-dep-cache/
|
||||||
|
WORKDIR /tmp/create-dep-cache
|
||||||
|
RUN make ci-all && rm -rf /tmp/create-dep-cache
|
||||||
|
|
||||||
WORKDIR /workspaces/immich
|
|
||||||
|
|
||||||
FROM dev-container-server AS dev-container-mobile
|
FROM dev-container-server AS dev-container-mobile
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
# Enable multiarch for arm64 if necessary
|
# Enable multiarch for arm64 if necessary
|
||||||
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
|
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
|
||||||
sudo dpkg --add-architecture amd64 && \
|
dpkg --add-architecture amd64 && \
|
||||||
sudo apt-get install -y --no-install-recommends \
|
apt-get update && \
|
||||||
qemu-user-static \
|
apt-get install -y --no-install-recommends \
|
||||||
libc6:amd64 \
|
qemu-user-static \
|
||||||
libstdc++6:amd64 \
|
libc6:amd64 \
|
||||||
libgcc1:amd64 && \
|
libstdc++6:amd64 \
|
||||||
rm -rf /var/lib/apt/lists; \
|
libgcc1:amd64; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Flutter SDK
|
# Flutter SDK
|
||||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
||||||
@@ -78,68 +62,60 @@ RUN mkdir -p ${FLUTTER_HOME} \
|
|||||||
&& rm flutter.tar.xz \
|
&& rm flutter.tar.xz \
|
||||||
&& chown -R node ${FLUTTER_HOME}
|
&& chown -R node ${FLUTTER_HOME}
|
||||||
|
|
||||||
RUN wget -qO- https://dcm.dev/pgp-key.public | gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
|
USER node
|
||||||
&& echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | tee /etc/apt/sources.list.d/dart_stable.list \
|
RUN sudo apt-get update \
|
||||||
&& apt-get update \
|
&& wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
|
||||||
&& apt-get install dcm -y && \
|
&& echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list \
|
||||||
&& rm -rf /var/lib/apt/lists
|
&& sudo apt-get update \
|
||||||
|
&& sudo apt-get install dcm -y
|
||||||
|
|
||||||
COPY --chmod=777 ../.devcontainer/mobile/container-mobile-post-create.sh /immich-devcontainer/container-mobile-post-create.sh
|
COPY --chmod=777 ../.devcontainer/mobile/container-mobile-post-create.sh /immich-devcontainer/container-mobile-post-create.sh
|
||||||
|
|
||||||
RUN dart --disable-analytics
|
RUN dart --disable-analytics
|
||||||
USER node
|
|
||||||
|
|
||||||
# server production build
|
FROM dev AS prod
|
||||||
FROM dev AS server-prod
|
|
||||||
|
|
||||||
RUN pnpm --filter immich install --frozen-lockfile && \
|
COPY server .
|
||||||
pnpm --filter immich build && \
|
RUN npm run build
|
||||||
pnpm --filter immich --prod --no-optional deploy /output/server-pruned
|
RUN npm prune --omit=dev --omit=optional
|
||||||
|
COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
|
||||||
|
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
|
||||||
|
|
||||||
# web production build
|
# web build
|
||||||
FROM dev AS web-prod
|
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS web
|
||||||
|
|
||||||
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY ./web ./web/
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
COPY ./i18n ./i18n/
|
RUN npm ci
|
||||||
RUN pnpm install --filter @immich/sdk --filter immich-web --frozen-lockfile && \
|
COPY open-api/typescript-sdk/ ./
|
||||||
pnpm --filter @immich/sdk build && \
|
RUN npm run build
|
||||||
pnpm --filter immich-web build
|
|
||||||
|
|
||||||
FROM dev AS cli-prod
|
|
||||||
|
|
||||||
COPY ./cli ./cli/
|
|
||||||
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
|
|
||||||
RUN pnpm install --filter @immich/sdk --filter @immich/cli --frozen-lockfile && \
|
|
||||||
pnpm --filter @immich/sdk build && \
|
|
||||||
pnpm --filter @immich/cli build && \
|
|
||||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
|
||||||
|
|
||||||
# prod build
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:commit-95312641aeba3ecaab9d73d05ca5d9746ab633b6
|
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
NVIDIA_DRIVER_CAPABILITIES=all \
|
|
||||||
NVIDIA_VISIBLE_DEVICES=all \
|
|
||||||
COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
|
||||||
|
|
||||||
RUN corepack enable && \
|
|
||||||
corepack install -g pnpm && \
|
|
||||||
mkdir -p /usr/src/app/upload
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY --from=server-prod /output/server-pruned/dist ./dist
|
COPY web/package*.json web/svelte.config.js ./
|
||||||
COPY --from=server-prod /output/server-pruned/bin ./bin
|
RUN npm ci
|
||||||
COPY --from=server-prod /output/server-pruned/package.json ./
|
COPY web ./
|
||||||
COPY --from=server-prod /output/server-pruned/node_modules/ ./node_modules
|
COPY i18n ../i18n
|
||||||
COPY --from=web-prod /usr/src/app/web/build /build/www
|
RUN npm run build
|
||||||
COPY --from=cli-prod /output/cli-pruned ./cli
|
|
||||||
RUN ln -S ./cli/bin/immich /usr/src/app/bin/immich
|
|
||||||
COPY server/resources ./resources/
|
# prod build
|
||||||
COPY server/start*.sh docker/scripts/get-cpus.sh ./
|
FROM ghcr.io/immich-app/base-server-prod:202505061115@sha256:9971d3a089787f0bd01f4682141d3665bcf5efb3e101a88e394ffd25bee4eedb
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
NVIDIA_DRIVER_CAPABILITIES=all \
|
||||||
|
NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||||
|
COPY --from=prod /usr/src/app/dist ./dist
|
||||||
|
COPY --from=prod /usr/src/app/bin ./bin
|
||||||
|
COPY --from=web /usr/src/app/build /build/www
|
||||||
|
COPY server/resources resources
|
||||||
|
COPY server/package.json server/package-lock.json ./
|
||||||
|
COPY server/start*.sh ./
|
||||||
|
COPY "docker/scripts/get-cpus.sh" ./
|
||||||
|
RUN npm install -g @immich/cli && npm cache clean --force
|
||||||
COPY LICENSE /licenses/LICENSE.txt
|
COPY LICENSE /licenses/LICENSE.txt
|
||||||
COPY LICENSE /LICENSE
|
COPY LICENSE /LICENSE
|
||||||
|
|
||||||
ENV PATH="${PATH}:/usr/src/app/bin"
|
ENV PATH="${PATH}:/usr/src/app/bin"
|
||||||
|
|
||||||
ARG BUILD_ID
|
ARG BUILD_ID
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cd /usr/src/app || exit
|
node /usr/src/app/node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch -- "$@"
|
||||||
FROZEN=1 make install-server
|
|
||||||
cd /usr/src/app/server || exit
|
|
||||||
pnpm exec nest start --debug "0.0.0.0:9230" --watch -- "$@"
|
|
||||||
|
|||||||
Generated
+18496
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -176,6 +176,5 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "^0.34.2"
|
"sharp": "^0.34.2"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export interface SystemConfig {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
storageLabelClaim: string;
|
storageLabelClaim: string;
|
||||||
storageQuotaClaim: string;
|
storageQuotaClaim: string;
|
||||||
|
roleClaim: string;
|
||||||
};
|
};
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -263,6 +264,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
profileSigningAlgorithm: 'none',
|
profileSigningAlgorithm: 'none',
|
||||||
storageLabelClaim: 'preferred_username',
|
storageLabelClaim: 'preferred_username',
|
||||||
storageQuotaClaim: 'immich_quota',
|
storageQuotaClaim: 'immich_quota',
|
||||||
|
roleClaim: 'immich_role',
|
||||||
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
|
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -395,6 +395,9 @@ class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
storageQuotaClaim!: string;
|
storageQuotaClaim!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
roleClaim!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigPasswordLoginDto {
|
class SystemConfigPasswordLoginDto {
|
||||||
|
|||||||
@@ -711,6 +711,7 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
isAdmin: false,
|
||||||
name: ' ',
|
name: ' ',
|
||||||
oauthId: user.oauthId,
|
oauthId: user.oauthId,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
@@ -739,6 +740,7 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
isAdmin: false,
|
||||||
name: ' ',
|
name: ' ',
|
||||||
oauthId: user.oauthId,
|
oauthId: user.oauthId,
|
||||||
quotaSizeInBytes: 5_368_709_120,
|
quotaSizeInBytes: 5_368_709_120,
|
||||||
@@ -805,6 +807,93 @@ describe(AuthService.name, () => {
|
|||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||||
expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled();
|
expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only allow "admin" and "user" for the role claim', async () => {
|
||||||
|
const user = factory.userAdmin({ oauthId: 'oauth-id' });
|
||||||
|
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
|
||||||
|
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'foo' });
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||||
|
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
|
||||||
|
mocks.user.getByOAuthId.mockResolvedValue(void 0);
|
||||||
|
mocks.user.create.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.callback(
|
||||||
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
|
email: user.email,
|
||||||
|
name: ' ',
|
||||||
|
oauthId: user.oauthId,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
storageLabel: null,
|
||||||
|
isAdmin: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an admin user if the role claim is set to admin', async () => {
|
||||||
|
const user = factory.userAdmin({ oauthId: 'oauth-id' });
|
||||||
|
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
|
||||||
|
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'admin' });
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||||
|
mocks.user.getByOAuthId.mockResolvedValue(void 0);
|
||||||
|
mocks.user.create.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.callback(
|
||||||
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
|
email: user.email,
|
||||||
|
name: ' ',
|
||||||
|
oauthId: user.oauthId,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
storageLabel: null,
|
||||||
|
isAdmin: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a custom role claim', async () => {
|
||||||
|
const user = factory.userAdmin({ oauthId: 'oauth-id' });
|
||||||
|
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
|
oauth: { ...systemConfigStub.oauthWithAutoRegister, roleClaim: 'my_role' },
|
||||||
|
});
|
||||||
|
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, my_role: 'admin' });
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||||
|
mocks.user.getByOAuthId.mockResolvedValue(void 0);
|
||||||
|
mocks.user.create.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.callback(
|
||||||
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
|
email: user.email,
|
||||||
|
name: ' ',
|
||||||
|
oauthId: user.oauthId,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
storageLabel: null,
|
||||||
|
isAdmin: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('link', () => {
|
describe('link', () => {
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export class AuthService extends BaseService {
|
|||||||
const { oauth } = await this.getConfig({ withCache: false });
|
const { oauth } = await this.getConfig({ withCache: false });
|
||||||
const url = this.resolveRedirectUri(oauth, dto.url);
|
const url = this.resolveRedirectUri(oauth, dto.url);
|
||||||
const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier);
|
const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier);
|
||||||
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth;
|
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;
|
||||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||||
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
||||||
|
|
||||||
@@ -290,6 +290,11 @@ export class AuthService extends BaseService {
|
|||||||
default: defaultStorageQuota,
|
default: defaultStorageQuota,
|
||||||
isValid: (value: unknown) => Number(value) >= 0,
|
isValid: (value: unknown) => Number(value) >= 0,
|
||||||
});
|
});
|
||||||
|
const role = this.getClaim<'admin' | 'user'>(profile, {
|
||||||
|
key: roleClaim,
|
||||||
|
default: 'user',
|
||||||
|
isValid: (value: unknown) => isString(value) && ['admin', 'user'].includes(value),
|
||||||
|
});
|
||||||
|
|
||||||
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
||||||
user = await this.createUser({
|
user = await this.createUser({
|
||||||
@@ -298,6 +303,7 @@ export class AuthService extends BaseService {
|
|||||||
oauthId: profile.sub,
|
oauthId: profile.sub,
|
||||||
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
|
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
|
||||||
storageLabel: storageLabel || null,
|
storageLabel: storageLabel || null,
|
||||||
|
isAdmin: role === 'admin',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
storageLabelClaim: 'preferred_username',
|
storageLabelClaim: 'preferred_username',
|
||||||
storageQuotaClaim: 'immich_quota',
|
storageQuotaClaim: 'immich_quota',
|
||||||
|
roleClaim: 'immich_role',
|
||||||
},
|
},
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
+3
-13
@@ -1,20 +1,10 @@
|
|||||||
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e
|
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e
|
||||||
ENV COREPACK_ENABLE_AUTO_PIN=0 \
|
|
||||||
COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
|
||||||
|
|
||||||
RUN corepack enable && corepack install -g pnpm && \
|
|
||||||
apk add --no-cache tini make && \
|
|
||||||
mkdir -p /buildcache/pnpm-store && \
|
|
||||||
chmod 777 -R /buildcache && \
|
|
||||||
pnpm config set store-dir /buildcache/pnpm-store
|
|
||||||
|
|
||||||
|
RUN apk add --no-cache tini
|
||||||
USER node
|
USER node
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY --chown=node:node ./open-api ./open-api/
|
COPY --chown=node:node package*.json ./
|
||||||
COPY --chown=node:node ./web ./web/
|
RUN npm ci
|
||||||
COPY --chown=node:node ./Makefile ./package* ./pnpm* ./
|
|
||||||
RUN make setup-web-dev
|
|
||||||
WORKDIR /usr/src/app/web
|
|
||||||
ENV CHOKIDAR_USEPOLLING=true
|
ENV CHOKIDAR_USEPOLLING=true
|
||||||
EXPOSE 24678
|
EXPOSE 24678
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
+13
-12
@@ -1,20 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
echo "Setup dev env"
|
TYPESCRIPT_SDK=/usr/src/open-api/typescript-sdk
|
||||||
(
|
|
||||||
cd /usr/src/app || exit
|
npm --prefix "$TYPESCRIPT_SDK" install
|
||||||
FROZEN=1 OFFLINE=1 make setup-web-dev
|
npm --prefix "$TYPESCRIPT_SDK" run build
|
||||||
)
|
|
||||||
|
|
||||||
COUNT=0
|
COUNT=0
|
||||||
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}"
|
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}"
|
||||||
until wget --spider --quiet "${UPSTREAM}/api/server/config" >/dev/null 2>&1; do
|
until wget --spider --quiet "${UPSTREAM}/api/server/config" > /dev/null 2>&1; do
|
||||||
if [ $((COUNT % 10)) -eq 0 ]; then
|
if [ $((COUNT % 10)) -eq 0 ]; then
|
||||||
echo "Waiting for $UPSTREAM to start..."
|
echo "Waiting for $UPSTREAM to start..."
|
||||||
fi
|
fi
|
||||||
COUNT=$((COUNT + 1))
|
COUNT=$((COUNT + 1))
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Connected to $UPSTREAM"
|
echo "Connected to $UPSTREAM"
|
||||||
pnpm exec vite dev --host 0.0.0.0 --port 3000
|
|
||||||
|
node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000
|
||||||
|
|||||||
Generated
+10471
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -107,6 +107,5 @@
|
|||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.0"
|
"node": "22.17.0"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,16 @@
|
|||||||
isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)}
|
isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label={$t('admin.oauth_role_claim').toUpperCase()}
|
||||||
|
description={$t('admin.oauth_role_claim_description')}
|
||||||
|
bind:value={config.oauth.roleClaim}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !config.oauth.enabled}
|
||||||
|
isEdited={!(config.oauth.roleClaim == savedConfig.oauth.roleClaim)}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
|
label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
const { value, width, alt = $t('alt_text_qr_code') }: Props = $props();
|
const { value, width, alt = $t('alt_text_qr_code') }: Props = $props();
|
||||||
|
|
||||||
let promise = $derived(QRCode.toDataURL(value, { margin: 0, width }));
|
let promise = $derived(QRCode.toDataURL(value, { margin: 4, width }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="width: {width}px; height: {width}px">
|
<div style="width: {width}px; height: {width}px">
|
||||||
|
|||||||
Reference in New Issue
Block a user