Compare commits
17 Commits
chore/pnpm
...
fix-scrubb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65de064ed5 | ||
|
|
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
|
||||
export CI=1 FROZEN=1 OFFLINE=1
|
||||
run_cmd make setup-web-dev setup-server-dev
|
||||
run_cmd make setup-dev
|
||||
)
|
||||
log ""
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@ cd "${IMMICH_WORKSPACE}/server" || (
|
||||
exit 1
|
||||
)
|
||||
|
||||
CI=1 pnpm install
|
||||
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 ..."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
@@ -16,7 +16,7 @@ until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_
|
||||
done
|
||||
|
||||
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 ..."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
.vscode/
|
||||
.github/
|
||||
.git/
|
||||
.env*
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
**/Dockerfile
|
||||
**/node_modules/
|
||||
**/.pnpm-store/
|
||||
**/dist/
|
||||
**/coverage/
|
||||
**/build/
|
||||
|
||||
design/
|
||||
docker/
|
||||
Dockerfile
|
||||
!docker/scripts
|
||||
|
||||
docs/
|
||||
!docs/package.json
|
||||
!docs/package-lock.json
|
||||
|
||||
e2e/
|
||||
!e2e/package.json
|
||||
!e2e/package-lock.json
|
||||
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
mobile/
|
||||
|
||||
open-api/typescript-sdk/build/
|
||||
!open-api/typescript-sdk/package.json
|
||||
!open-api/typescript-sdk/package-lock.json
|
||||
cli/coverage/
|
||||
cli/dist/
|
||||
cli/node_modules/
|
||||
cli/Dockerfile
|
||||
|
||||
open-api/typescript-sdk/build/
|
||||
open-api/typescript-sdk/node_modules/
|
||||
|
||||
server/coverage/
|
||||
server/node_modules/
|
||||
server/upload/
|
||||
server/src/queries
|
||||
server/dist/
|
||||
server/www/
|
||||
server/Dockerfile
|
||||
|
||||
web/node_modules/
|
||||
web/coverage/
|
||||
web/.svelte-kit
|
||||
web/build/
|
||||
web/.env
|
||||
web/Dockerfile
|
||||
|
||||
28
.github/package-lock.json
generated
vendored
Normal file
28
.github/package-lock.json
generated
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.github/workflows/cli.yml
vendored
27
.github/workflows/cli.yml
vendored
@@ -33,24 +33,21 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
node-version-file: './cli/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: pnpm install && pnpm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm publish
|
||||
- name: Prepare SDK
|
||||
run: npm ci --prefix ../open-api/typescript-sdk/
|
||||
- name: Build SDK
|
||||
run: npm run build --prefix ../open-api/typescript-sdk/
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm publish
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
17
.github/workflows/docs-build.yml
vendored
17
.github/workflows/docs-build.yml
vendored
@@ -53,24 +53,21 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './docs/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run install
|
||||
run: pnpm install
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format
|
||||
run: npm run format
|
||||
|
||||
- name: Run build
|
||||
run: pnpm build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build output
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Fix formatting
|
||||
run: make install-all && make format-all
|
||||
|
||||
13
.github/workflows/sdk.yml
vendored
13
.github/workflows/sdk.yml
vendored
@@ -20,21 +20,18 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
run: npm run build
|
||||
- name: Publish
|
||||
run: pnpm publish
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
24
.github/workflows/static_analysis.yml
vendored
24
.github/workflows/static_analysis.yml
vendored
@@ -42,6 +42,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./mobile
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -56,27 +59,23 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Install DCM
|
||||
run: |
|
||||
sudo apt-get update
|
||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
||||
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
|
||||
sudo apt-get update
|
||||
sudo apt-get install dcm
|
||||
# TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
|
||||
uses: bo0tzz/setup-dcm@b4952ab813659c03513b57bd78bfe3f634171f8a
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: auto
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Generate translation file
|
||||
run: make translation
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run Build Runner
|
||||
run: make build
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Generate platform API
|
||||
run: make pigeon
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
@@ -98,19 +97,16 @@ jobs:
|
||||
|
||||
- name: Run dart analyze
|
||||
run: dart analyze --fatal-infos
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run dart format
|
||||
run: dart format lib/ --set-exit-if-changed
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run dart custom_lint
|
||||
run: dart run custom_lint
|
||||
working-directory: ./mobile
|
||||
|
||||
# TODO: Use https://github.com/CQLabs/dcm-action
|
||||
- name: Run DCM
|
||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||
working-directory: ./mobile
|
||||
|
||||
zizmor:
|
||||
name: zizmor
|
||||
|
||||
214
.github/workflows/test.yml
vendored
214
.github/workflows/test.yml
vendored
@@ -80,33 +80,30 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run package manager install
|
||||
run: pnpm install
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
run: npm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run tsc
|
||||
run: pnpm check
|
||||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run small tests & coverage
|
||||
run: pnpm test
|
||||
run: npm test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-unit-tests:
|
||||
@@ -126,37 +123,34 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: pnpm install && pnpm run build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
run: npm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run tsc
|
||||
run: pnpm check
|
||||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: pnpm test
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-unit-tests-win:
|
||||
@@ -176,30 +170,27 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
# Skip linter & formatter in Windows test.
|
||||
- name: Run tsc
|
||||
run: pnpm check
|
||||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: pnpm test
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
web-lint:
|
||||
@@ -219,33 +210,30 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Run npm install
|
||||
run: pnpm rebuild && pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm lint:p
|
||||
run: npm run lint:p
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run svelte checks
|
||||
run: pnpm check:svelte
|
||||
run: npm run check:svelte
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
web-unit-tests:
|
||||
@@ -265,29 +253,26 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Run npm install
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
- name: Run tsc
|
||||
run: pnpm check:typescript
|
||||
run: npm run check:typescript
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: pnpm test
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
i18n-tests:
|
||||
@@ -303,21 +288,18 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=immich-web install --frozen-lockfile
|
||||
run: npm --prefix=web ci
|
||||
|
||||
- name: Format
|
||||
run: pnpm --filter=immich-web format:i18n
|
||||
run: npm --prefix=web run format:i18n
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
@@ -352,35 +334,32 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
run: npm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run tsc
|
||||
run: pnpm check
|
||||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
server-medium-tests:
|
||||
@@ -400,21 +379,18 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
- name: Run medium tests
|
||||
run: pnpm test:medium
|
||||
run: npm run test:medium
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
e2e-tests-server-cli:
|
||||
@@ -438,33 +414,25 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
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
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./cli
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Docker build
|
||||
@@ -472,7 +440,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run e2e tests (api & cli)
|
||||
run: pnpm test
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
e2e-tests-web:
|
||||
@@ -496,23 +464,20 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
@@ -619,21 +584,18 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
node-version-file: './.github/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
shellcheck:
|
||||
@@ -665,21 +627,18 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install server dependencies
|
||||
run: pnpm --filter immich install --frozen-lockfile
|
||||
run: npm --prefix=server ci
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm --filter immich build
|
||||
run: npm --prefix=server run build
|
||||
|
||||
- name: Run API generation
|
||||
run: make open-api
|
||||
@@ -731,31 +690,28 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install server dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: npm ci
|
||||
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
run: npm run build
|
||||
|
||||
- name: Run existing migrations
|
||||
run: pnpm migrations:run
|
||||
run: npm run migrations:run
|
||||
|
||||
- name: Test npm run schema:reset command works
|
||||
run: pnpm schema:reset
|
||||
run: npm run schema:reset
|
||||
|
||||
- name: Generate new migrations
|
||||
continue-on-error: true
|
||||
run: pnpm migrations:generate src/TestMigration
|
||||
run: npm run migrations:generate src/TestMigration
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
@@ -774,7 +730,7 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Run SQL generation
|
||||
run: pnpm sync:sql
|
||||
run: npm run sync:sql
|
||||
env:
|
||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,4 +24,3 @@ mobile/android/fastlane/report.xml
|
||||
mobile/ios/fastlane/report.xml
|
||||
|
||||
vite.config.js.timestamp-*
|
||||
.pnpm-store
|
||||
|
||||
71
Makefile
71
Makefile
@@ -40,7 +40,7 @@ open-api-typescript:
|
||||
cd ./open-api && bash ./bin/generate-open-api.sh typescript
|
||||
|
||||
sql:
|
||||
pnpm --filter immich run sync:sql
|
||||
npm --prefix server run sync:sql
|
||||
|
||||
attach-server:
|
||||
docker exec -it docker_immich-server_1 sh
|
||||
@@ -50,40 +50,31 @@ renovate:
|
||||
|
||||
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-%:
|
||||
pnpm --filter $(call map-package,$*) audit fix
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
||||
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-web: build-sdk
|
||||
build-%: install-%
|
||||
pnpm --filter $(call map-package,$*) run build
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
|
||||
format-%:
|
||||
pnpm --filter $(call map-package,$*) run format:fix
|
||||
npm --prefix $* run format:fix
|
||||
lint-%:
|
||||
pnpm --filter $(call map-package,$*) run lint:fix
|
||||
lint-web:
|
||||
pnpm --filter $(call map-package,$*) run lint:p
|
||||
npm --prefix $* run lint:fix
|
||||
check-%:
|
||||
pnpm --filter $(call map-package,$*) run check
|
||||
npm --prefix $* run check
|
||||
check-web:
|
||||
pnpm --filter immich-web run check:typescript
|
||||
pnpm --filter immich-web run check:svelte
|
||||
npm --prefix web run check:typescript
|
||||
npm --prefix web run check:svelte
|
||||
test-%:
|
||||
pnpm --filter $(call map-package,$*) run test
|
||||
npm --prefix $* run test
|
||||
test-e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml build
|
||||
pnpm --filter immich-e2e run test
|
||||
pnpm --filter immich-e2e run test:web
|
||||
npm --prefix e2e run test
|
||||
npm --prefix e2e run test:web
|
||||
test-medium:
|
||||
docker run \
|
||||
--rm \
|
||||
@@ -93,28 +84,19 @@ test-medium:
|
||||
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
|
||||
-e NODE_ENV=development \
|
||||
immich-server:latest \
|
||||
-c "pnpm test:medium -- --run"
|
||||
-c "npm ci && npm run test:medium -- --run"
|
||||
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:
|
||||
pnpm -r --filter '!documentation' install
|
||||
|
||||
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
|
||||
|
||||
check-all:
|
||||
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
|
||||
lint-all:
|
||||
pnpm -r --filter '!documentation' run lint:fix
|
||||
format-all:
|
||||
pnpm -r --filter '!documentation' run format:fix
|
||||
audit-all:
|
||||
pnpm -r --filter '!documentation' audit fix
|
||||
hygiene-all: audit-all
|
||||
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
|
||||
|
||||
test-all:
|
||||
pnpm -r --filter '!documentation' run "/^test/"
|
||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$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) ;
|
||||
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
||||
audit-all: $(foreach M,$(MODULES),audit-$M) ;
|
||||
hygiene-all: lint-all format-all check-all sql audit-all;
|
||||
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
|
||||
|
||||
clean:
|
||||
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 ./e2e/docker-compose.yml rm -v -f || true
|
||||
|
||||
setup-server-dev: install-server
|
||||
setup-web-dev: install-sdk build-sdk install-web
|
||||
setup-dev: install-server install-sdk build-sdk install-web
|
||||
@@ -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:
|
||||
|
||||
# if you don't have node installed
|
||||
$ npm install -g pnpm
|
||||
$ pnpm install
|
||||
$ pnpm build
|
||||
$ npm install
|
||||
$ npm run build
|
||||
|
||||
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:
|
||||
|
||||
# if you don't have node installed
|
||||
$ npm install -g pnpm
|
||||
$ pnpm install
|
||||
$ pnpm build
|
||||
$ npm install
|
||||
$ npm run build
|
||||
$ ts-node .
|
||||
|
||||
You'll need ts-node, the easiest way to install it is to use npm:
|
||||
|
||||
4617
cli/package-lock.json
generated
Normal file
4617
cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ name: immich-dev
|
||||
services:
|
||||
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
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
@@ -27,18 +27,14 @@ services:
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ../Makefile:/usr/src/app/Makefile
|
||||
- ../package.json:/usr/src/app/package.json
|
||||
- ../pnpm-lock.yaml:/usr/src/app/pnpm-lock.yaml
|
||||
- ../pnpm-workspace.yaml:/usr/src/app/pnpm-workspace.yaml
|
||||
- ../server:/usr/src/app/server
|
||||
- ../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
|
||||
- ../server:/usr/src/app
|
||||
- ../open-api:/usr/src/open-api
|
||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||
- /usr/src/app/node_modules
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
# user: ${UID:-1000}:${GID:-1000}
|
||||
environment:
|
||||
IMMICH_REPOSITORY: immich-app/immich
|
||||
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
|
||||
@@ -72,25 +68,20 @@ services:
|
||||
image: immich-web-dev:latest
|
||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
||||
# user: 0:0
|
||||
# user: ${UID:-1000}:${GID:-1000}
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: web/Dockerfile
|
||||
command: ['/usr/src/app/web/bin/immich-web']
|
||||
context: ../web
|
||||
command: ['/usr/src/app/bin/immich-web']
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 24678:24678
|
||||
volumes:
|
||||
- ../Makefile:/usr/src/app/Makefile
|
||||
- ../package.json:/usr/src/app/package.json
|
||||
- ../pnpm-lock.yaml:/usr/src/app/pnpm-lock.yaml
|
||||
- ../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
|
||||
- ../web:/usr/src/app
|
||||
- ../i18n:/usr/src/i18n
|
||||
- ../open-api/:/usr/src/open-api/
|
||||
# - ../../ui:/usr/ui
|
||||
- /usr/src/app/node_modules
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
|
||||
@@ -1,7 +1,2 @@
|
||||
build/
|
||||
.docusaurus/
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
@@ -5,7 +5,7 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ pnpm install
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### 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) |
|
||||
| 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**¹** |
|
||||
| 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**¹** |
|
||||
| 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 |
|
||||
|
||||
@@ -200,7 +200,7 @@ When the Dev Container starts, it automatically:
|
||||
|
||||
1. **Runs post-create script** (`container-server-post-create.sh`):
|
||||
- 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`
|
||||
|
||||
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 -`
|
||||
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
|
||||
|
||||
```bash
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
### Unit tests
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||
| `HEIF` | `.heif` | :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: | |
|
||||
| `PNG` | `.png` | :white_check_mark: | |
|
||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||
|
||||
20545
docs/package-lock.json
generated
Normal file
20545
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
7409
e2e/package-lock.json
generated
Normal file
7409
e2e/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -227,6 +227,21 @@ describe(`/oauth`, () => {
|
||||
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 () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum OAuthUser {
|
||||
NO_NAME = 'no-name',
|
||||
WITH_QUOTA = 'with-quota',
|
||||
WITH_USERNAME = 'with-username',
|
||||
WITH_ROLE = 'with-role',
|
||||
}
|
||||
|
||||
const claims = [
|
||||
@@ -34,6 +35,12 @@ const claims = [
|
||||
preferred_username: 'user-quota',
|
||||
immich_quota: 25,
|
||||
},
|
||||
{
|
||||
sub: OAuthUser.WITH_ROLE,
|
||||
email: 'oauth-with-role@immich.app',
|
||||
email_verified: true,
|
||||
immich_role: 'admin',
|
||||
},
|
||||
];
|
||||
|
||||
const withDefaultClaims = (sub: string) => ({
|
||||
@@ -64,7 +71,15 @@ const setup = async () => {
|
||||
claims: {
|
||||
openid: ['sub'],
|
||||
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: {
|
||||
jwtUserinfo: {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const tempDir = tmpdir();
|
||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||
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[]) =>
|
||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||
|
||||
@@ -196,6 +196,8 @@
|
||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||
"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_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_description": "Manage OAuth login settings",
|
||||
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
||||
|
||||
1332
mobile/drift_schemas/main/drift_schema_v1.json
generated
1332
mobile/drift_schemas/main/drift_schema_v1.json
generated
File diff suppressed because one or more lines are too long
84
mobile/lib/domain/models/stack.model.dart
Normal file
84
mobile/lib/domain/models/stack.model.dart
Normal file
@@ -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());
|
||||
case SyncEntityType.memoryToAssetDeleteV1:
|
||||
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:
|
||||
_logger.warning("Unknown sync data type: $type");
|
||||
}
|
||||
|
||||
@@ -53,6 +53,43 @@ class TimelineFactory {
|
||||
bucketSource: () =>
|
||||
_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 {
|
||||
|
||||
@@ -9,8 +9,57 @@ class BackgroundSyncManager {
|
||||
Cancelable<void>? _deviceAlbumSyncTask;
|
||||
Cancelable<void>? _hashTask;
|
||||
|
||||
Completer<void>? _localSyncMutex;
|
||||
Completer<void>? _remoteSyncMutex;
|
||||
Completer<void>? _hashMutex;
|
||||
|
||||
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() {
|
||||
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
|
||||
Future<void> syncLocal({bool full = false}) {
|
||||
if (_deviceAlbumSyncTask != null) {
|
||||
return _deviceAlbumSyncTask!.future;
|
||||
}
|
||||
return _withLocalSyncMutex(() async {
|
||||
if (_deviceAlbumSyncTask != null) {
|
||||
return _deviceAlbumSyncTask!.future;
|
||||
}
|
||||
|
||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||
// captured by the closure passed to [runInIsolateGentle].
|
||||
_deviceAlbumSyncTask = full
|
||||
? runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: true),
|
||||
)
|
||||
: runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: false),
|
||||
);
|
||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||
// captured by the closure passed to [runInIsolateGentle].
|
||||
_deviceAlbumSyncTask = full
|
||||
? runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: true),
|
||||
)
|
||||
: runInIsolateGentle(
|
||||
computation: (ref) =>
|
||||
ref.read(localSyncServiceProvider).sync(full: false),
|
||||
);
|
||||
|
||||
return _deviceAlbumSyncTask!.whenComplete(() {
|
||||
_deviceAlbumSyncTask = null;
|
||||
return _deviceAlbumSyncTask!.whenComplete(() {
|
||||
_deviceAlbumSyncTask = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// No need to cancel the task, as it can also be run when the user logs out
|
||||
Future<void> hashAssets() {
|
||||
if (_hashTask != null) {
|
||||
return _hashTask!.future;
|
||||
}
|
||||
return _withHashMutex(() async {
|
||||
if (_hashTask != null) {
|
||||
return _hashTask!.future;
|
||||
}
|
||||
|
||||
_hashTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||
);
|
||||
return _hashTask!.whenComplete(() {
|
||||
_hashTask = null;
|
||||
_hashTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||
);
|
||||
return _hashTask!.whenComplete(() {
|
||||
_hashTask = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncRemote() {
|
||||
if (_syncTask != null) {
|
||||
return _syncTask!.future;
|
||||
}
|
||||
return _withRemoteSyncMutex(() async {
|
||||
if (_syncTask != null) {
|
||||
return _syncTask!.future;
|
||||
}
|
||||
|
||||
_syncTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||
);
|
||||
return _syncTask!.whenComplete(() {
|
||||
_syncTask = null;
|
||||
_syncTask = runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
||||
);
|
||||
return _syncTask!.whenComplete(() {
|
||||
_syncTask = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
22
mobile/lib/infrastructure/entities/stack.entity.dart
Normal file
22
mobile/lib/infrastructure/entities/stack.entity.dart
Normal file
@@ -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};
|
||||
}
|
||||
706
mobile/lib/infrastructure/entities/stack.entity.drift.dart
generated
Normal file
706
mobile/lib/infrastructure/entities/stack.entity.drift.dart
generated
Normal file
@@ -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_user.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_metadata.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
@@ -50,6 +51,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
RemoteAlbumUserEntity,
|
||||
MemoryEntity,
|
||||
MemoryAssetEntity,
|
||||
StackEntity,
|
||||
],
|
||||
include: {
|
||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||
|
||||
@@ -27,9 +27,11 @@ import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
as i12;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
as i13;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
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 {
|
||||
$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 i13.$MemoryAssetEntityTable memoryAssetEntity =
|
||||
i13.$MemoryAssetEntityTable(this);
|
||||
i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this)
|
||||
.accessor<i14.MergedAssetDrift>(i14.MergedAssetDrift.new);
|
||||
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
|
||||
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
|
||||
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@@ -80,7 +83,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity
|
||||
memoryAssetEntity,
|
||||
stackEntity
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||
@@ -205,6 +209,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
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
|
||||
@@ -242,4 +253,6 @@ class $DriftManager {
|
||||
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i14.$$StackEntityTableTableManager get stackEntity =>
|
||||
i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
}
|
||||
|
||||
30
mobile/lib/infrastructure/repositories/stack.repository.dart
Normal file
30
mobile/lib/infrastructure/repositories/stack.repository.dart
Normal file
@@ -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.memoriesV1,
|
||||
SyncRequestType.memoryToAssetsV1,
|
||||
SyncRequestType.stacksV1,
|
||||
SyncRequestType.partnerStacksV1,
|
||||
],
|
||||
).toJson(),
|
||||
);
|
||||
@@ -163,6 +165,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
||||
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.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 {
|
||||
|
||||
@@ -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_asset.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/repositories/db.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -69,8 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: SyncPartnerDeleteV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -92,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: SyncPartnerV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -104,10 +105,10 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}) async {
|
||||
try {
|
||||
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) {
|
||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -142,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -186,11 +187,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
} catch (error, stack) {
|
||||
_logger.severe(
|
||||
'Error: updateAssetsExifV1 - $debugLabel',
|
||||
error,
|
||||
stackTrace,
|
||||
stack,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
@@ -201,8 +202,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
await _db.remoteAlbumEntity.deleteWhere(
|
||||
(row) => row.id.isIn(data.map((e) => e.albumId)),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAlbumsV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -229,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAlbumsV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -248,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAlbumUsersV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -275,11 +276,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
} catch (error, stack) {
|
||||
_logger.severe(
|
||||
'Error: updateAlbumUsersV1 - $debugLabel',
|
||||
error,
|
||||
stackTrace,
|
||||
stack,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
@@ -300,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteAlbumToAssetsV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -325,11 +326,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
} catch (error, stack) {
|
||||
_logger.severe(
|
||||
'Error: updateAlbumToAssetsV1 - $debugLabel',
|
||||
error,
|
||||
stackTrace,
|
||||
stack,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
@@ -359,8 +360,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateMemoriesV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateMemoriesV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -370,8 +371,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
await _db.memoryEntity.deleteWhere(
|
||||
(row) => row.id.isIn(data.map((e) => e.memoryId)),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteMemoriesV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: deleteMemoriesV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -392,8 +393,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: updateMemoryAssetsV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateMemoryAssetsV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -413,8 +414,49 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace);
|
||||
} catch (error, stack) {
|
||||
_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;
|
||||
}
|
||||
}
|
||||
@@ -467,7 +509,7 @@ extension on String {
|
||||
Duration? toDuration() {
|
||||
try {
|
||||
final parts = split(':')
|
||||
.map((error) => double.parse(error).toInt())
|
||||
.map((e) => double.parse(e).toInt())
|
||||
.toList(growable: false);
|
||||
|
||||
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())
|
||||
.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> {
|
||||
|
||||
@@ -118,7 +118,7 @@ class TabShellPage extends ConsumerWidget {
|
||||
const MainTimelineRoute(),
|
||||
SearchRoute(),
|
||||
const DriftAlbumsRoute(),
|
||||
const LibraryRoute(),
|
||||
const DriftLibraryRoute(),
|
||||
],
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||
|
||||
33
mobile/lib/presentation/pages/dev/drift_archive.page.dart
Normal file
33
mobile/lib/presentation/pages/dev/drift_archive.page.dart
Normal file
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
mobile/lib/presentation/pages/dev/drift_favorite.page.dart
Normal file
33
mobile/lib/presentation/pages/dev/drift_favorite.page.dart
Normal file
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
mobile/lib/presentation/pages/dev/drift_trash.page.dart
Normal file
33
mobile/lib/presentation/pages/dev/drift_trash.page.dart
Normal file
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
mobile/lib/presentation/pages/dev/drift_video.page.dart
Normal file
33
mobile/lib/presentation/pages/dev/drift_video.page.dart
Normal file
@@ -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.remoteAlbumUserEntity.deleteAll();
|
||||
await db.remoteAlbumAssetEntity.deleteAll();
|
||||
await db.memoryEntity.deleteAll();
|
||||
await db.memoryAssetEntity.deleteAll();
|
||||
await db.stackEntity.deleteAll();
|
||||
},
|
||||
),
|
||||
_Feature(
|
||||
@@ -96,6 +99,11 @@ final _features = [
|
||||
icon: Icons.timeline_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Video',
|
||||
icon: Icons.video_collection_outlined,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
|
||||
),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
|
||||
@@ -162,6 +162,10 @@ final _remoteStats = [
|
||||
name: 'Memories Assets',
|
||||
load: (db) => db.managers.memoryAssetEntity.count(),
|
||||
),
|
||||
_Stat(
|
||||
name: 'Stacks',
|
||||
load: (db) => db.managers.stackEntity.count(),
|
||||
),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
|
||||
501
mobile/lib/presentation/pages/drift_library.page.dart
Normal file
501
mobile/lib/presentation/pages/drift_library.page.dart
Normal file
@@ -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,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: _thumbTopOffset + widget.topPadding,
|
||||
end: 0,
|
||||
child: RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onVerticalDragStart: _onDragStart,
|
||||
onVerticalDragUpdate: _onDragUpdate,
|
||||
onVerticalDragEnd: _onDragEnd,
|
||||
child: _Scrubber(
|
||||
thumbAnimation: _thumbAnimation,
|
||||
labelAnimation: _labelAnimation,
|
||||
label: label,
|
||||
if (_scrollController.hasClients &&
|
||||
_scrollController.position.maxScrollExtent > 0)
|
||||
PositionedDirectional(
|
||||
top: _thumbTopOffset + widget.topPadding,
|
||||
end: 0,
|
||||
child: RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onVerticalDragStart: _onDragStart,
|
||||
onVerticalDragUpdate: _onDragUpdate,
|
||||
onVerticalDragEnd: _onDragEnd,
|
||||
child: _Scrubber(
|
||||
thumbAnimation: _thumbAnimation,
|
||||
labelAnimation: _labelAnimation,
|
||||
label: label,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
7
mobile/lib/providers/stack.provider.dart
Normal file
7
mobile/lib/providers/stack.provider.dart
Normal file
@@ -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.remoteAlbumAssetEntity.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/search.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/local_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/remote_timeline.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/widgets/asset_viewer/asset_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
@@ -174,7 +180,7 @@ class AppRouter extends RootStackRouter {
|
||||
maintainState: false,
|
||||
),
|
||||
AutoRoute(
|
||||
page: LibraryRoute.page,
|
||||
page: DriftLibraryRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
@@ -392,7 +398,30 @@ class AppRouter extends RootStackRouter {
|
||||
page: DriftMemoryRoute.page,
|
||||
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
|
||||
// auto_route_library#1722
|
||||
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
|
||||
/// [DriftMemoryPage]
|
||||
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
|
||||
/// [EditImagePage]
|
||||
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
||||
|
||||
@@ -73,7 +73,10 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
|
||||
onPressed: () {
|
||||
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||
ref.read(backgroundSyncProvider).syncRemote();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.sync,
|
||||
),
|
||||
|
||||
@@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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_metadata.model.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/widgets/common/transparent_image.dart';
|
||||
|
||||
@@ -26,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
bool isDarkTheme = context.themeData.brightness == Brightness.dark;
|
||||
final userAvatarColor = user.avatarColor.toColor();
|
||||
final profileImageUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||
|
||||
@@ -34,14 +32,14 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: isDarkTheme && user.avatarColor == AvatarColor.primary
|
||||
color: userAvatarColor.computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
),
|
||||
child: Text(user.name[0].toUpperCase()),
|
||||
);
|
||||
return CircleAvatar(
|
||||
backgroundColor: user.avatarColor.toColor(),
|
||||
backgroundColor: userAvatarColor,
|
||||
radius: radius,
|
||||
child: user.profileImagePath == null
|
||||
? textIcon
|
||||
|
||||
@@ -24,6 +24,7 @@ class SystemConfigOAuthDto {
|
||||
required this.mobileOverrideEnabled,
|
||||
required this.mobileRedirectUri,
|
||||
required this.profileSigningAlgorithm,
|
||||
required this.roleClaim,
|
||||
required this.scope,
|
||||
required this.signingAlgorithm,
|
||||
required this.storageLabelClaim,
|
||||
@@ -55,6 +56,8 @@ class SystemConfigOAuthDto {
|
||||
|
||||
String profileSigningAlgorithm;
|
||||
|
||||
String roleClaim;
|
||||
|
||||
String scope;
|
||||
|
||||
String signingAlgorithm;
|
||||
@@ -81,6 +84,7 @@ class SystemConfigOAuthDto {
|
||||
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
||||
other.mobileRedirectUri == mobileRedirectUri &&
|
||||
other.profileSigningAlgorithm == profileSigningAlgorithm &&
|
||||
other.roleClaim == roleClaim &&
|
||||
other.scope == scope &&
|
||||
other.signingAlgorithm == signingAlgorithm &&
|
||||
other.storageLabelClaim == storageLabelClaim &&
|
||||
@@ -102,6 +106,7 @@ class SystemConfigOAuthDto {
|
||||
(mobileOverrideEnabled.hashCode) +
|
||||
(mobileRedirectUri.hashCode) +
|
||||
(profileSigningAlgorithm.hashCode) +
|
||||
(roleClaim.hashCode) +
|
||||
(scope.hashCode) +
|
||||
(signingAlgorithm.hashCode) +
|
||||
(storageLabelClaim.hashCode) +
|
||||
@@ -110,7 +115,7 @@ class SystemConfigOAuthDto {
|
||||
(tokenEndpointAuthMethod.hashCode);
|
||||
|
||||
@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() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -129,6 +134,7 @@ class SystemConfigOAuthDto {
|
||||
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
||||
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
|
||||
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
|
||||
json[r'roleClaim'] = this.roleClaim;
|
||||
json[r'scope'] = this.scope;
|
||||
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
||||
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
||||
@@ -158,6 +164,7 @@ class SystemConfigOAuthDto {
|
||||
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
||||
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
|
||||
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
|
||||
roleClaim: mapValueOfType<String>(json, r'roleClaim')!,
|
||||
scope: mapValueOfType<String>(json, r'scope')!,
|
||||
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
||||
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
||||
@@ -222,6 +229,7 @@ class SystemConfigOAuthDto {
|
||||
'mobileOverrideEnabled',
|
||||
'mobileRedirectUri',
|
||||
'profileSigningAlgorithm',
|
||||
'roleClaim',
|
||||
'scope',
|
||||
'signingAlgorithm',
|
||||
'storageLabelClaim',
|
||||
|
||||
@@ -89,6 +89,18 @@ void main() {
|
||||
.thenAnswer(successHandler);
|
||||
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()))
|
||||
.thenAnswer(successHandler);
|
||||
when(
|
||||
() => mockSyncStreamRepo.updateStacksV1(
|
||||
any(),
|
||||
debugLabel: any(named: 'debugLabel'),
|
||||
),
|
||||
).thenAnswer(successHandler);
|
||||
when(
|
||||
() => mockSyncStreamRepo.deleteStacksV1(
|
||||
any(),
|
||||
debugLabel: any(named: 'debugLabel'),
|
||||
),
|
||||
).thenAnswer(successHandler);
|
||||
|
||||
sut = SyncStreamService(
|
||||
syncApiRepository: mockSyncApiRepo,
|
||||
|
||||
@@ -28,11 +28,11 @@ function dart {
|
||||
|
||||
function typescript {
|
||||
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
|
||||
(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
|
||||
dart
|
||||
|
||||
@@ -14654,6 +14654,9 @@
|
||||
"profileSigningAlgorithm": {
|
||||
"type": "string"
|
||||
},
|
||||
"roleClaim": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -14690,6 +14693,7 @@
|
||||
"mobileOverrideEnabled",
|
||||
"mobileRedirectUri",
|
||||
"profileSigningAlgorithm",
|
||||
"roleClaim",
|
||||
"scope",
|
||||
"signingAlgorithm",
|
||||
"storageLabelClaim",
|
||||
|
||||
@@ -5,7 +5,7 @@ A TypeScript SDK for interfacing with the [Immich](https://immich.app/) API.
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pnpm i --save @immich/sdk
|
||||
npm i --save @immich/sdk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
57
open-api/typescript-sdk/package-lock.json
generated
Normal file
57
open-api/typescript-sdk/package-lock.json
generated
Normal file
@@ -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": {
|
||||
"node": "22.17.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1398,6 +1398,7 @@ export type SystemConfigOAuthDto = {
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
profileSigningAlgorithm: string;
|
||||
roleClaim: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
storageLabelClaim: string;
|
||||
|
||||
10
package.json
10
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
25306
pnpm-lock.yaml
generated
25306
pnpm-lock.yaml
generated
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
|
||||
@@ -1,68 +1,52 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:commit-95312641aeba3ecaab9d73d05ca5d9746ab633b6 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
|
||||
FROM ghcr.io/immich-app/base-server-dev:202505131114@sha256:cf4507bbbf307e9b6d8ee9418993321f2b85867da8ce14d0a20ccaf9574cb995 AS dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./server ./server/
|
||||
COPY Makefile ./package* ./pnpm* ./
|
||||
|
||||
RUN umask 000 && mkdir -p /buildcache/pnpm-store && \
|
||||
pnpm config set store-dir /buildcache/pnpm-store && \
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true make setup-server-dev
|
||||
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci && \
|
||||
# exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
|
||||
# they're marked as optional dependencies, so we need to copy them manually after pruning
|
||||
rm -rf node_modules/@img/sharp-libvips* && \
|
||||
rm -rf node_modules/@img/sharp-linuxmusl-x64
|
||||
ENV PATH="${PATH}:/usr/src/app/bin" \
|
||||
IMMICH_ENV=development \
|
||||
NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
IMMICH_ENV=development \
|
||||
NVIDIA_DRIVER_CAPABILITIES=all \
|
||||
NVIDIA_VISIBLE_DEVICES=all
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
|
||||
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 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
|
||||
|
||||
USER root
|
||||
# Enable multiarch for arm64 if necessary
|
||||
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
|
||||
sudo dpkg --add-architecture amd64 && \
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
libc6:amd64 \
|
||||
libstdc++6:amd64 \
|
||||
libgcc1:amd64 && \
|
||||
rm -rf /var/lib/apt/lists; \
|
||||
fi
|
||||
dpkg --add-architecture amd64 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
libc6:amd64 \
|
||||
libstdc++6:amd64 \
|
||||
libgcc1:amd64; \
|
||||
fi
|
||||
|
||||
# Flutter SDK
|
||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
||||
@@ -78,68 +62,60 @@ RUN mkdir -p ${FLUTTER_HOME} \
|
||||
&& rm flutter.tar.xz \
|
||||
&& chown -R node ${FLUTTER_HOME}
|
||||
|
||||
RUN wget -qO- https://dcm.dev/pgp-key.public | gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
|
||||
&& 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 \
|
||||
&& apt-get update \
|
||||
&& apt-get install dcm -y && \
|
||||
&& rm -rf /var/lib/apt/lists
|
||||
USER node
|
||||
RUN sudo apt-get update \
|
||||
&& wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
|
||||
&& 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 \
|
||||
&& 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
|
||||
|
||||
RUN dart --disable-analytics
|
||||
USER node
|
||||
|
||||
# server production build
|
||||
FROM dev AS server-prod
|
||||
FROM dev AS prod
|
||||
|
||||
RUN pnpm --filter immich install --frozen-lockfile && \
|
||||
pnpm --filter immich build && \
|
||||
pnpm --filter immich --prod --no-optional deploy /output/server-pruned
|
||||
COPY server .
|
||||
RUN npm run build
|
||||
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
|
||||
FROM dev AS web-prod
|
||||
# web build
|
||||
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS web
|
||||
|
||||
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
|
||||
COPY ./web ./web/
|
||||
COPY ./i18n ./i18n/
|
||||
RUN pnpm install --filter @immich/sdk --filter immich-web --frozen-lockfile && \
|
||||
pnpm --filter @immich/sdk 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/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
RUN npm ci
|
||||
COPY open-api/typescript-sdk/ ./
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=server-prod /output/server-pruned/dist ./dist
|
||||
COPY --from=server-prod /output/server-pruned/bin ./bin
|
||||
COPY --from=server-prod /output/server-pruned/package.json ./
|
||||
COPY --from=server-prod /output/server-pruned/node_modules/ ./node_modules
|
||||
COPY --from=web-prod /usr/src/app/web/build /build/www
|
||||
COPY --from=cli-prod /output/cli-pruned ./cli
|
||||
RUN ln -S ./cli/bin/immich /usr/src/app/bin/immich
|
||||
COPY server/resources ./resources/
|
||||
COPY server/start*.sh docker/scripts/get-cpus.sh ./
|
||||
COPY web/package*.json web/svelte.config.js ./
|
||||
RUN npm ci
|
||||
COPY web ./
|
||||
COPY i18n ../i18n
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
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 /LICENSE
|
||||
|
||||
ENV PATH="${PATH}:/usr/src/app/bin"
|
||||
|
||||
ARG BUILD_ID
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd /usr/src/app || exit
|
||||
FROZEN=1 make install-server
|
||||
cd /usr/src/app/server || exit
|
||||
pnpm exec nest start --debug "0.0.0.0:9230" --watch -- "$@"
|
||||
node /usr/src/app/node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch -- "$@"
|
||||
|
||||
18496
server/package-lock.json
generated
Normal file
18496
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,5 @@
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.34.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface SystemConfig {
|
||||
timeout: number;
|
||||
storageLabelClaim: string;
|
||||
storageQuotaClaim: string;
|
||||
roleClaim: string;
|
||||
};
|
||||
passwordLogin: {
|
||||
enabled: boolean;
|
||||
@@ -263,6 +264,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
profileSigningAlgorithm: 'none',
|
||||
storageLabelClaim: 'preferred_username',
|
||||
storageQuotaClaim: 'immich_quota',
|
||||
roleClaim: 'immich_role',
|
||||
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
|
||||
timeout: 30_000,
|
||||
},
|
||||
|
||||
@@ -395,6 +395,9 @@ class SystemConfigOAuthDto {
|
||||
|
||||
@IsString()
|
||||
storageQuotaClaim!: string;
|
||||
|
||||
@IsString()
|
||||
roleClaim!: string;
|
||||
}
|
||||
|
||||
class SystemConfigPasswordLoginDto {
|
||||
|
||||
@@ -711,6 +711,7 @@ describe(AuthService.name, () => {
|
||||
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||
email: user.email,
|
||||
isAdmin: false,
|
||||
name: ' ',
|
||||
oauthId: user.oauthId,
|
||||
quotaSizeInBytes: 0,
|
||||
@@ -739,6 +740,7 @@ describe(AuthService.name, () => {
|
||||
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||
email: user.email,
|
||||
isAdmin: false,
|
||||
name: ' ',
|
||||
oauthId: user.oauthId,
|
||||
quotaSizeInBytes: 5_368_709_120,
|
||||
@@ -805,6 +807,93 @@ describe(AuthService.name, () => {
|
||||
expect(mocks.user.update).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', () => {
|
||||
|
||||
@@ -250,7 +250,7 @@ export class AuthService extends BaseService {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
const url = this.resolveRedirectUri(oauth, dto.url);
|
||||
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)}`);
|
||||
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
||||
|
||||
@@ -290,6 +290,11 @@ export class AuthService extends BaseService {
|
||||
default: defaultStorageQuota,
|
||||
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 || ''}`;
|
||||
user = await this.createUser({
|
||||
@@ -298,6 +303,7 @@ export class AuthService extends BaseService {
|
||||
oauthId: profile.sub,
|
||||
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
|
||||
storageLabel: storageLabel || null,
|
||||
isAdmin: role === 'admin',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
timeout: 30_000,
|
||||
storageLabelClaim: 'preferred_username',
|
||||
storageQuotaClaim: 'immich_quota',
|
||||
roleClaim: 'immich_role',
|
||||
},
|
||||
passwordLogin: {
|
||||
enabled: true,
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
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
|
||||
WORKDIR /usr/src/app
|
||||
COPY --chown=node:node ./open-api ./open-api/
|
||||
COPY --chown=node:node ./web ./web/
|
||||
COPY --chown=node:node ./Makefile ./package* ./pnpm* ./
|
||||
RUN make setup-web-dev
|
||||
WORKDIR /usr/src/app/web
|
||||
COPY --chown=node:node package*.json ./
|
||||
RUN npm ci
|
||||
ENV CHOKIDAR_USEPOLLING=true
|
||||
EXPOSE 24678
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
echo "Setup dev env"
|
||||
(
|
||||
cd /usr/src/app || exit
|
||||
FROZEN=1 OFFLINE=1 make setup-web-dev
|
||||
)
|
||||
TYPESCRIPT_SDK=/usr/src/open-api/typescript-sdk
|
||||
|
||||
npm --prefix "$TYPESCRIPT_SDK" install
|
||||
npm --prefix "$TYPESCRIPT_SDK" run build
|
||||
|
||||
|
||||
COUNT=0
|
||||
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}"
|
||||
until wget --spider --quiet "${UPSTREAM}/api/server/config" >/dev/null 2>&1; do
|
||||
if [ $((COUNT % 10)) -eq 0 ]; then
|
||||
echo "Waiting for $UPSTREAM to start..."
|
||||
fi
|
||||
COUNT=$((COUNT + 1))
|
||||
sleep 1
|
||||
until wget --spider --quiet "${UPSTREAM}/api/server/config" > /dev/null 2>&1; do
|
||||
if [ $((COUNT % 10)) -eq 0 ]; then
|
||||
echo "Waiting for $UPSTREAM to start..."
|
||||
fi
|
||||
COUNT=$((COUNT + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
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
|
||||
|
||||
10471
web/package-lock.json
generated
Normal file
10471
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -107,6 +107,5 @@
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.17.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,16 @@
|
||||
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
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
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>
|
||||
|
||||
<div style="width: {width}px; height: {width}px">
|
||||
|
||||
Reference in New Issue
Block a user