mirror of
https://github.com/immich-app/immich.git
synced 2025-12-15 09:13:11 +03:00
Compare commits
17 Commits
feature/re
...
chore/log-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa4c6aa562 | ||
|
|
4b345e02ff | ||
|
|
8cf900bafa | ||
|
|
59a3f0f455 | ||
|
|
c5d99711f7 | ||
|
|
4c0a41723f | ||
|
|
f73511a754 | ||
|
|
e637387082 | ||
|
|
baad38f0e6 | ||
|
|
161147af51 | ||
|
|
cbdf5011f9 | ||
|
|
f0f1d279c4 | ||
|
|
5821f2fe61 | ||
|
|
4cbce072be | ||
|
|
5e5bb7e87d | ||
|
|
b052893a1e | ||
|
|
15e58595fd |
2
.github/workflows/build-mobile.yml
vendored
2
.github/workflows/build-mobile.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/fix-format.yml
vendored
4
.github/workflows/fix-format.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
2
.github/workflows/merge-translations.yml
vendored
2
.github/workflows/merge-translations.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
8
.github/workflows/prepare-release.yml
vendored
8
.github/workflows/prepare-release.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.IMMICH_VERSION }}
|
||||
|
||||
6
.github/workflows/release-pr.yml
vendored
6
.github/workflows/release-pr.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.result }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
2
.github/workflows/sdk.yml
vendored
2
.github/workflows/sdk.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
26
.github/workflows/test.yml
vendored
26
.github/workflows/test.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -387,7 +387,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -481,7 +481,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -617,7 +617,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './.github/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -668,7 +668,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -730,7 +730,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -127,7 +127,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:49214755b6153f90a597adcbff0252cc61069f8ab69ce8411285cd4a560e8038
|
||||
image: prom/prometheus@sha256:d936808bdea528155c0154a922cd42fd75716b8bb7ba302641350f9f3eaeba09
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -24,6 +24,9 @@ server {
|
||||
# disable buffering uploads to prevent OOM on reverse proxy server and make uploads twice as fast (no pause)
|
||||
proxy_request_buffering off;
|
||||
|
||||
# increase body buffer to avoid limiting upload speed
|
||||
client_body_buffer_size 1024k;
|
||||
|
||||
# Set headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -52,7 +52,7 @@ Password login has been enabled.
|
||||
Disable Maintenance Mode
|
||||
|
||||
```
|
||||
immich-admin disable-maintenace-mode
|
||||
immich-admin disable-maintenance-mode
|
||||
Maintenance mode has been disabled.
|
||||
```
|
||||
|
||||
|
||||
@@ -1667,8 +1667,6 @@
|
||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||
"query_asset_id": "Query Asset ID",
|
||||
"queue_status": "Queuing {count}/{total}",
|
||||
"quick_actions_settings_description": "Drag to rearrange buttons. Up to {count} available buttons are displayed in order.",
|
||||
"quick_actions_settings_title": "Button order settings",
|
||||
"rating": "Star rating",
|
||||
"rating_clear": "Clear rating",
|
||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||
@@ -1729,7 +1727,6 @@
|
||||
"removed_photo_from_memory": "Removed photo from memory",
|
||||
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
||||
"rename": "Rename",
|
||||
"reorder_buttons": "Reorder buttons",
|
||||
"repair": "Repair",
|
||||
"repair_no_results_message": "Untracked and missing files will show up here",
|
||||
"replace_with_upload": "Replace with upload",
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
|
||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
|
||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
|
||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
|
||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
|
||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -153,6 +156,13 @@
|
||||
path = WidgetExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FEE084F22EC172080045228E /* Schemas */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Schemas;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -160,6 +170,9 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -254,6 +267,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FEE084F22EC172080045228E /* Schemas */,
|
||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||
@@ -341,6 +355,7 @@
|
||||
fileSystemSynchronizedGroups = (
|
||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||
FEE084F22EC172080045228E /* Schemas */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
@@ -419,6 +434,10 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -1201,6 +1220,43 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pointfreeco/sqlite-data";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.3.0;
|
||||
};
|
||||
};
|
||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-http-structured-headers.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.5.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
FEE084F72EC172460045228E /* SQLiteData */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */;
|
||||
productName = SQLiteData;
|
||||
};
|
||||
FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
||||
productName = RawStructuredFieldValues;
|
||||
};
|
||||
FEE084FC2EC1725A0045228E /* StructuredFieldValues */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
||||
productName = StructuredFieldValues;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||
"state" : {
|
||||
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
|
||||
"version" : "1.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grdb.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/groue/GRDB.swift",
|
||||
"state" : {
|
||||
"revision" : "18497b68fdbb3a09528d260a0a0e1e7e61c8c53d",
|
||||
"version" : "7.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sqlite-data",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/sqlite-data",
|
||||
"state" : {
|
||||
"revision" : "b66b894b9a5710f1072c8eb6448a7edfc2d743d9",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-case-paths",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-case-paths",
|
||||
"state" : {
|
||||
"revision" : "6989976265be3f8d2b5802c722f9ba168e227c71",
|
||||
"version" : "1.7.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-clocks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||
"state" : {
|
||||
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
||||
"version" : "1.0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections",
|
||||
"state" : {
|
||||
"revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-concurrency-extras",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||
"state" : {
|
||||
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
||||
"version" : "1.3.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||
"state" : {
|
||||
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||
"version" : "1.3.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-dependencies",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
||||
"state" : {
|
||||
"revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc",
|
||||
"version" : "1.10.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-http-structured-headers",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-http-structured-headers.git",
|
||||
"state" : {
|
||||
"revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb",
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-identified-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-identified-collections",
|
||||
"state" : {
|
||||
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-perception",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-perception",
|
||||
"state" : {
|
||||
"revision" : "4f47ebafed5f0b0172cf5c661454fa8e28fb2ac4",
|
||||
"version" : "2.0.9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-sharing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-sharing",
|
||||
"state" : {
|
||||
"revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818",
|
||||
"version" : "2.7.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-snapshot-testing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "a8b7c5e0ed33d8ab8887d1654d9b59f2cbad529b",
|
||||
"version" : "1.18.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-structured-queries",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-structured-queries",
|
||||
"state" : {
|
||||
"revision" : "1447ea20550f6f02c4b48cc80931c3ed40a9c756",
|
||||
"version" : "0.25.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||
"state" : {
|
||||
"revision" : "4799286537280063c85a32f09884cfbca301b1a1",
|
||||
"version" : "602.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-tagged",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-tagged",
|
||||
"state" : {
|
||||
"revision" : "3907a9438f5b57d317001dc99f3f11b46882272b",
|
||||
"version" : "0.10.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "4c27acf5394b645b70d8ba19dc249c0472d5f618",
|
||||
"version" : "1.7.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -137,8 +137,7 @@
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>We need local network permission to connect to the local server using IP address and
|
||||
allow the casting feature to work</string>
|
||||
<string>We need local network permission to connect to the local server using IP address and allow the casting feature to work</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
|
||||
177
mobile/ios/Runner/Schemas/Constants.swift
Normal file
177
mobile/ios/Runner/Schemas/Constants.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
import SQLiteData
|
||||
|
||||
struct Endpoint: Codable {
|
||||
let url: URL
|
||||
let status: Status
|
||||
|
||||
enum Status: String, Codable {
|
||||
case loading, valid, error, unknown
|
||||
}
|
||||
}
|
||||
|
||||
enum StoreKey: Int, CaseIterable, QueryBindable {
|
||||
// MARK: - Int
|
||||
case _version = 0
|
||||
static let version = Typed<Int>(rawValue: ._version)
|
||||
case _deviceIdHash = 3
|
||||
static let deviceIdHash = Typed<Int>(rawValue: ._deviceIdHash)
|
||||
case _backupTriggerDelay = 8
|
||||
static let backupTriggerDelay = Typed<Int>(rawValue: ._backupTriggerDelay)
|
||||
case _tilesPerRow = 103
|
||||
static let tilesPerRow = Typed<Int>(rawValue: ._tilesPerRow)
|
||||
case _groupAssetsBy = 105
|
||||
static let groupAssetsBy = Typed<Int>(rawValue: ._groupAssetsBy)
|
||||
case _uploadErrorNotificationGracePeriod = 106
|
||||
static let uploadErrorNotificationGracePeriod = Typed<Int>(rawValue: ._uploadErrorNotificationGracePeriod)
|
||||
case _thumbnailCacheSize = 110
|
||||
static let thumbnailCacheSize = Typed<Int>(rawValue: ._thumbnailCacheSize)
|
||||
case _imageCacheSize = 111
|
||||
static let imageCacheSize = Typed<Int>(rawValue: ._imageCacheSize)
|
||||
case _albumThumbnailCacheSize = 112
|
||||
static let albumThumbnailCacheSize = Typed<Int>(rawValue: ._albumThumbnailCacheSize)
|
||||
case _selectedAlbumSortOrder = 113
|
||||
static let selectedAlbumSortOrder = Typed<Int>(rawValue: ._selectedAlbumSortOrder)
|
||||
case _logLevel = 115
|
||||
static let logLevel = Typed<Int>(rawValue: ._logLevel)
|
||||
case _mapRelativeDate = 119
|
||||
static let mapRelativeDate = Typed<Int>(rawValue: ._mapRelativeDate)
|
||||
case _mapThemeMode = 124
|
||||
static let mapThemeMode = Typed<Int>(rawValue: ._mapThemeMode)
|
||||
|
||||
// MARK: - String
|
||||
case _assetETag = 1
|
||||
static let assetETag = Typed<String>(rawValue: ._assetETag)
|
||||
case _currentUser = 2
|
||||
static let currentUser = Typed<String>(rawValue: ._currentUser)
|
||||
case _deviceId = 4
|
||||
static let deviceId = Typed<String>(rawValue: ._deviceId)
|
||||
case _accessToken = 11
|
||||
static let accessToken = Typed<String>(rawValue: ._accessToken)
|
||||
case _serverEndpoint = 12
|
||||
static let serverEndpoint = Typed<String>(rawValue: ._serverEndpoint)
|
||||
case _sslClientCertData = 15
|
||||
static let sslClientCertData = Typed<String>(rawValue: ._sslClientCertData)
|
||||
case _sslClientPasswd = 16
|
||||
static let sslClientPasswd = Typed<String>(rawValue: ._sslClientPasswd)
|
||||
case _themeMode = 102
|
||||
static let themeMode = Typed<String>(rawValue: ._themeMode)
|
||||
case _customHeaders = 127
|
||||
static let customHeaders = Typed<[String: String]>(rawValue: ._customHeaders)
|
||||
case _primaryColor = 128
|
||||
static let primaryColor = Typed<String>(rawValue: ._primaryColor)
|
||||
case _preferredWifiName = 133
|
||||
static let preferredWifiName = Typed<String>(rawValue: ._preferredWifiName)
|
||||
|
||||
// MARK: - Endpoint
|
||||
case _externalEndpointList = 135
|
||||
static let externalEndpointList = Typed<[Endpoint]>(rawValue: ._externalEndpointList)
|
||||
|
||||
// MARK: - URL
|
||||
case _localEndpoint = 134
|
||||
static let localEndpoint = Typed<URL>(rawValue: ._localEndpoint)
|
||||
case _serverUrl = 10
|
||||
static let serverUrl = Typed<URL>(rawValue: ._serverUrl)
|
||||
|
||||
// MARK: - Date
|
||||
case _backupFailedSince = 5
|
||||
static let backupFailedSince = Typed<Date>(rawValue: ._backupFailedSince)
|
||||
|
||||
// MARK: - Bool
|
||||
case _backupRequireWifi = 6
|
||||
static let backupRequireWifi = Typed<Bool>(rawValue: ._backupRequireWifi)
|
||||
case _backupRequireCharging = 7
|
||||
static let backupRequireCharging = Typed<Bool>(rawValue: ._backupRequireCharging)
|
||||
case _autoBackup = 13
|
||||
static let autoBackup = Typed<Bool>(rawValue: ._autoBackup)
|
||||
case _backgroundBackup = 14
|
||||
static let backgroundBackup = Typed<Bool>(rawValue: ._backgroundBackup)
|
||||
case _loadPreview = 100
|
||||
static let loadPreview = Typed<Bool>(rawValue: ._loadPreview)
|
||||
case _loadOriginal = 101
|
||||
static let loadOriginal = Typed<Bool>(rawValue: ._loadOriginal)
|
||||
case _dynamicLayout = 104
|
||||
static let dynamicLayout = Typed<Bool>(rawValue: ._dynamicLayout)
|
||||
case _backgroundBackupTotalProgress = 107
|
||||
static let backgroundBackupTotalProgress = Typed<Bool>(rawValue: ._backgroundBackupTotalProgress)
|
||||
case _backgroundBackupSingleProgress = 108
|
||||
static let backgroundBackupSingleProgress = Typed<Bool>(rawValue: ._backgroundBackupSingleProgress)
|
||||
case _storageIndicator = 109
|
||||
static let storageIndicator = Typed<Bool>(rawValue: ._storageIndicator)
|
||||
case _advancedTroubleshooting = 114
|
||||
static let advancedTroubleshooting = Typed<Bool>(rawValue: ._advancedTroubleshooting)
|
||||
case _preferRemoteImage = 116
|
||||
static let preferRemoteImage = Typed<Bool>(rawValue: ._preferRemoteImage)
|
||||
case _loopVideo = 117
|
||||
static let loopVideo = Typed<Bool>(rawValue: ._loopVideo)
|
||||
case _mapShowFavoriteOnly = 118
|
||||
static let mapShowFavoriteOnly = Typed<Bool>(rawValue: ._mapShowFavoriteOnly)
|
||||
case _selfSignedCert = 120
|
||||
static let selfSignedCert = Typed<Bool>(rawValue: ._selfSignedCert)
|
||||
case _mapIncludeArchived = 121
|
||||
static let mapIncludeArchived = Typed<Bool>(rawValue: ._mapIncludeArchived)
|
||||
case _ignoreIcloudAssets = 122
|
||||
static let ignoreIcloudAssets = Typed<Bool>(rawValue: ._ignoreIcloudAssets)
|
||||
case _selectedAlbumSortReverse = 123
|
||||
static let selectedAlbumSortReverse = Typed<Bool>(rawValue: ._selectedAlbumSortReverse)
|
||||
case _mapwithPartners = 125
|
||||
static let mapwithPartners = Typed<Bool>(rawValue: ._mapwithPartners)
|
||||
case _enableHapticFeedback = 126
|
||||
static let enableHapticFeedback = Typed<Bool>(rawValue: ._enableHapticFeedback)
|
||||
case _dynamicTheme = 129
|
||||
static let dynamicTheme = Typed<Bool>(rawValue: ._dynamicTheme)
|
||||
case _colorfulInterface = 130
|
||||
static let colorfulInterface = Typed<Bool>(rawValue: ._colorfulInterface)
|
||||
case _syncAlbums = 131
|
||||
static let syncAlbums = Typed<Bool>(rawValue: ._syncAlbums)
|
||||
case _autoEndpointSwitching = 132
|
||||
static let autoEndpointSwitching = Typed<Bool>(rawValue: ._autoEndpointSwitching)
|
||||
case _loadOriginalVideo = 136
|
||||
static let loadOriginalVideo = Typed<Bool>(rawValue: ._loadOriginalVideo)
|
||||
case _manageLocalMediaAndroid = 137
|
||||
static let manageLocalMediaAndroid = Typed<Bool>(rawValue: ._manageLocalMediaAndroid)
|
||||
case _readonlyModeEnabled = 138
|
||||
static let readonlyModeEnabled = Typed<Bool>(rawValue: ._readonlyModeEnabled)
|
||||
case _autoPlayVideo = 139
|
||||
static let autoPlayVideo = Typed<Bool>(rawValue: ._autoPlayVideo)
|
||||
case _photoManagerCustomFilter = 1000
|
||||
static let photoManagerCustomFilter = Typed<Bool>(rawValue: ._photoManagerCustomFilter)
|
||||
case _betaPromptShown = 1001
|
||||
static let betaPromptShown = Typed<Bool>(rawValue: ._betaPromptShown)
|
||||
case _betaTimeline = 1002
|
||||
static let betaTimeline = Typed<Bool>(rawValue: ._betaTimeline)
|
||||
case _enableBackup = 1003
|
||||
static let enableBackup = Typed<Bool>(rawValue: ._enableBackup)
|
||||
case _useWifiForUploadVideos = 1004
|
||||
static let useWifiForUploadVideos = Typed<Bool>(rawValue: ._useWifiForUploadVideos)
|
||||
case _useWifiForUploadPhotos = 1005
|
||||
static let useWifiForUploadPhotos = Typed<Bool>(rawValue: ._useWifiForUploadPhotos)
|
||||
case _needBetaMigration = 1006
|
||||
static let needBetaMigration = Typed<Bool>(rawValue: ._needBetaMigration)
|
||||
case _shouldResetSync = 1007
|
||||
static let shouldResetSync = Typed<Bool>(rawValue: ._shouldResetSync)
|
||||
|
||||
struct Typed<T>: RawRepresentable {
|
||||
let rawValue: StoreKey
|
||||
|
||||
@_transparent
|
||||
init(rawValue value: StoreKey) {
|
||||
self.rawValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum BackupSelection: Int, QueryBindable {
|
||||
case selected, none, excluded
|
||||
}
|
||||
|
||||
enum AvatarColor: Int, QueryBindable {
|
||||
case primary, pink, red, yellow, blue, green, purple, orange, gray, amber
|
||||
}
|
||||
|
||||
enum AlbumUserRole: Int, QueryBindable {
|
||||
case editor, viewer
|
||||
}
|
||||
|
||||
enum MemoryType: Int, QueryBindable {
|
||||
case onThisDay
|
||||
}
|
||||
146
mobile/ios/Runner/Schemas/Store.swift
Normal file
146
mobile/ios/Runner/Schemas/Store.swift
Normal file
@@ -0,0 +1,146 @@
|
||||
import SQLiteData
|
||||
|
||||
enum StoreError: Error {
|
||||
case invalidJSON(String)
|
||||
case invalidURL(String)
|
||||
case encodingFailed
|
||||
}
|
||||
|
||||
protocol StoreConvertible {
|
||||
associatedtype StorageType
|
||||
static func fromValue(_ value: StorageType) throws(StoreError) -> Self
|
||||
static func toValue(_ value: Self) throws(StoreError) -> StorageType
|
||||
}
|
||||
|
||||
extension Int: StoreConvertible {
|
||||
static func fromValue(_ value: Int) -> Int { value }
|
||||
static func toValue(_ value: Int) -> Int { value }
|
||||
}
|
||||
|
||||
extension Bool: StoreConvertible {
|
||||
static func fromValue(_ value: Int) -> Bool { value == 1 }
|
||||
static func toValue(_ value: Bool) -> Int { value ? 1 : 0 }
|
||||
}
|
||||
|
||||
extension Date: StoreConvertible {
|
||||
static func fromValue(_ value: Int) -> Date { Date(timeIntervalSince1970: TimeInterval(value) / 1000) }
|
||||
static func toValue(_ value: Date) -> Int { Int(value.timeIntervalSince1970 * 1000) }
|
||||
}
|
||||
|
||||
extension String: StoreConvertible {
|
||||
static func fromValue(_ value: String) -> String { value }
|
||||
static func toValue(_ value: String) -> String { value }
|
||||
}
|
||||
|
||||
extension URL: StoreConvertible {
|
||||
static func fromValue(_ value: String) throws(StoreError) -> URL {
|
||||
guard let url = URL(string: value) else {
|
||||
throw StoreError.invalidURL(value)
|
||||
}
|
||||
return url
|
||||
}
|
||||
static func toValue(_ value: URL) -> String { value.absoluteString }
|
||||
}
|
||||
|
||||
extension StoreConvertible where Self: Codable, StorageType == String {
|
||||
static var jsonDecoder: JSONDecoder { JSONDecoder() }
|
||||
static var jsonEncoder: JSONEncoder { JSONEncoder() }
|
||||
|
||||
static func fromValue(_ value: String) throws(StoreError) -> Self {
|
||||
do {
|
||||
return try jsonDecoder.decode(Self.self, from: Data(value.utf8))
|
||||
} catch {
|
||||
throw StoreError.invalidJSON(value)
|
||||
}
|
||||
}
|
||||
|
||||
static func toValue(_ value: Self) throws(StoreError) -> String {
|
||||
let encoded: Data
|
||||
do {
|
||||
encoded = try jsonEncoder.encode(value)
|
||||
} catch {
|
||||
throw StoreError.encodingFailed
|
||||
}
|
||||
|
||||
guard let string = String(data: encoded, encoding: .utf8) else {
|
||||
throw StoreError.encodingFailed
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: StoreConvertible where Element: Codable {
|
||||
typealias StorageType = String
|
||||
}
|
||||
|
||||
extension Dictionary: StoreConvertible where Key == String, Value: Codable {
|
||||
typealias StorageType = String
|
||||
}
|
||||
|
||||
class StoreRepository {
|
||||
private let db: DatabasePool
|
||||
|
||||
init(db: DatabasePool) {
|
||||
self.db = db
|
||||
}
|
||||
|
||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) throws -> T? where T.StorageType == Int {
|
||||
let query = Store.select(\.intValue).where { $0.id.eq(key.rawValue) }
|
||||
if let value = try db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
||||
return try T.fromValue(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) throws -> T? where T.StorageType == String {
|
||||
let query = Store.select(\.stringValue).where { $0.id.eq(key.rawValue) }
|
||||
if let value = try db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
||||
return try T.fromValue(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) async throws -> T? where T.StorageType == Int {
|
||||
let query = Store.select(\.intValue).where { $0.id.eq(key.rawValue) }
|
||||
if let value = try await db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
||||
return try T.fromValue(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) async throws -> T? where T.StorageType == String {
|
||||
let query = Store.select(\.stringValue).where { $0.id.eq(key.rawValue) }
|
||||
if let value = try await db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
||||
return try T.fromValue(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) throws where T.StorageType == Int {
|
||||
let value = try T.toValue(value)
|
||||
try db.write { conn in
|
||||
try Store.upsert { Store(id: key.rawValue, stringValue: nil, intValue: value) }.execute(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) throws where T.StorageType == String {
|
||||
let value = try T.toValue(value)
|
||||
try db.write { conn in
|
||||
try Store.upsert { Store(id: key.rawValue, stringValue: value, intValue: nil) }.execute(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) async throws where T.StorageType == Int {
|
||||
let value = try T.toValue(value)
|
||||
try await db.write { conn in
|
||||
try Store.upsert { Store(id: key.rawValue, stringValue: nil, intValue: value) }.execute(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) async throws where T.StorageType == String {
|
||||
let value = try T.toValue(value)
|
||||
try await db.write { conn in
|
||||
try Store.upsert { Store(id: key.rawValue, stringValue: value, intValue: nil) }.execute(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
237
mobile/ios/Runner/Schemas/Tables.swift
Normal file
237
mobile/ios/Runner/Schemas/Tables.swift
Normal file
@@ -0,0 +1,237 @@
|
||||
import GRDB
|
||||
import SQLiteData
|
||||
|
||||
@Table("asset_face_entity")
|
||||
struct AssetFace {
|
||||
let id: String
|
||||
let assetId: String
|
||||
let personId: String?
|
||||
let imageWidth: Int
|
||||
let imageHeight: Int
|
||||
let boundingBoxX1: Int
|
||||
let boundingBoxY1: Int
|
||||
let boundingBoxX2: Int
|
||||
let boundingBoxY2: Int
|
||||
let sourceType: String
|
||||
}
|
||||
|
||||
@Table("auth_user_entity")
|
||||
struct AuthUser {
|
||||
let id: String
|
||||
let name: String
|
||||
let email: String
|
||||
let isAdmin: Bool
|
||||
let hasProfileImage: Bool
|
||||
let profileChangedAt: Date
|
||||
let avatarColor: AvatarColor
|
||||
let quotaSizeInBytes: Int
|
||||
let quotaUsageInBytes: Int
|
||||
let pinCode: String?
|
||||
}
|
||||
|
||||
@Table("local_album_entity")
|
||||
struct LocalAlbum {
|
||||
let id: String
|
||||
let backupSelection: BackupSelection
|
||||
let linkedRemoteAlbumId: String?
|
||||
let marker_: Bool?
|
||||
let name: String
|
||||
let isIosSharedAlbum: Bool
|
||||
let updatedAt: Date
|
||||
}
|
||||
|
||||
@Table("local_album_asset_entity")
|
||||
struct LocalAlbumAsset {
|
||||
let id: ID
|
||||
let marker_: String?
|
||||
|
||||
@Selection
|
||||
struct ID {
|
||||
let assetId: String
|
||||
let albumId: String
|
||||
}
|
||||
}
|
||||
|
||||
@Table("local_asset_entity")
|
||||
struct LocalAsset {
|
||||
let id: String
|
||||
let checksum: String?
|
||||
let createdAt: Date
|
||||
let durationInSeconds: Int?
|
||||
let height: Int?
|
||||
let isFavorite: Bool
|
||||
let name: String
|
||||
let orientation: String
|
||||
let type: Int
|
||||
let updatedAt: Date
|
||||
let width: Int?
|
||||
}
|
||||
|
||||
@Table("memory_asset_entity")
|
||||
struct MemoryAsset {
|
||||
let id: ID
|
||||
|
||||
@Selection
|
||||
struct ID {
|
||||
let assetId: String
|
||||
let albumId: String
|
||||
}
|
||||
}
|
||||
|
||||
@Table("memory_entity")
|
||||
struct Memory {
|
||||
let id: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let deletedAt: Date?
|
||||
let ownerId: String
|
||||
let type: MemoryType
|
||||
let data: String
|
||||
let isSaved: Bool
|
||||
let memoryAt: Date
|
||||
let seenAt: Date?
|
||||
let showAt: Date?
|
||||
let hideAt: Date?
|
||||
}
|
||||
|
||||
@Table("partner_entity")
|
||||
struct Partner {
|
||||
let id: ID
|
||||
let inTimeline: Bool
|
||||
|
||||
@Selection
|
||||
struct ID {
|
||||
let sharedById: String
|
||||
let sharedWithId: String
|
||||
}
|
||||
}
|
||||
|
||||
@Table("person_entity")
|
||||
struct Person {
|
||||
let id: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let ownerId: String
|
||||
let name: String
|
||||
let faceAssetId: String?
|
||||
let isFavorite: Bool
|
||||
let isHidden: Bool
|
||||
let color: String?
|
||||
let birthDate: Date?
|
||||
}
|
||||
|
||||
@Table("remote_album_entity")
|
||||
struct RemoteAlbum {
|
||||
let id: String
|
||||
let createdAt: Date
|
||||
let description: String?
|
||||
let isActivityEnabled: Bool
|
||||
let name: String
|
||||
let order: Int
|
||||
let ownerId: String
|
||||
let thumbnailAssetId: String?
|
||||
let updatedAt: Date
|
||||
}
|
||||
|
||||
@Table("remote_album_asset_entity")
|
||||
struct RemoteAlbumAsset {
|
||||
let id: ID
|
||||
|
||||
@Selection
|
||||
struct ID {
|
||||
let assetId: String
|
||||
let albumId: String
|
||||
}
|
||||
}
|
||||
|
||||
@Table("remote_album_user_entity")
|
||||
struct RemoteAlbumUser {
|
||||
let id: ID
|
||||
let role: AlbumUserRole
|
||||
|
||||
@Selection
|
||||
struct ID {
|
||||
let albumId: String
|
||||
let userId: String
|
||||
}
|
||||
}
|
||||
|
||||
@Table("remote_asset_entity")
|
||||
struct RemoteAsset {
|
||||
let id: String
|
||||
let checksum: String?
|
||||
let deletedAt: Date?
|
||||
let isFavorite: Int
|
||||
let libraryId: String?
|
||||
let livePhotoVideoId: String?
|
||||
let localDateTime: Date?
|
||||
let orientation: String
|
||||
let ownerId: String
|
||||
let stackId: String?
|
||||
let visibility: Int
|
||||
}
|
||||
|
||||
@Table("remote_exif_entity")
|
||||
struct RemoteExif {
|
||||
@Column(primaryKey: true)
|
||||
let assetId: String
|
||||
let city: String?
|
||||
let state: String?
|
||||
let country: String?
|
||||
let dateTimeOriginal: Date?
|
||||
let description: String?
|
||||
let height: Int?
|
||||
let width: Int?
|
||||
let exposureTime: String?
|
||||
let fNumber: Double?
|
||||
let fileSize: Int?
|
||||
let focalLength: Double?
|
||||
let latitude: Double?
|
||||
let longitude: Double?
|
||||
let iso: Int?
|
||||
let make: String?
|
||||
let model: String?
|
||||
let lens: String?
|
||||
let orientation: String?
|
||||
let timeZone: String?
|
||||
let rating: Int?
|
||||
let projectionType: String?
|
||||
}
|
||||
|
||||
@Table("stack_entity")
|
||||
struct Stack {
|
||||
let id: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let ownerId: String
|
||||
let primaryAssetId: String
|
||||
}
|
||||
|
||||
@Table("store_entity")
|
||||
struct Store {
|
||||
let id: StoreKey
|
||||
let stringValue: String?
|
||||
let intValue: Int?
|
||||
}
|
||||
|
||||
@Table("user_entity")
|
||||
struct User {
|
||||
let id: String
|
||||
let name: String
|
||||
let email: String
|
||||
let hasProfileImage: Bool
|
||||
let profileChangedAt: Date
|
||||
let avatarColor: AvatarColor
|
||||
}
|
||||
|
||||
@Table("user_metadata_entity")
|
||||
struct UserMetadata {
|
||||
let id: ID
|
||||
let value: Data
|
||||
|
||||
@Selection
|
||||
struct ID {
|
||||
let userId: String
|
||||
let key: Date
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,6 @@ enum StoreKey<T> {
|
||||
|
||||
autoPlayVideo<bool>._(139),
|
||||
albumGridView<bool>._(140),
|
||||
viewerQuickActionOrder<String>._(141),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
class QuickActionService {
|
||||
final ActionButtonOrderRepository _repository;
|
||||
|
||||
const QuickActionService(this._repository);
|
||||
|
||||
static final Set<ActionButtonType> _quickActionSet = Set<ActionButtonType>.unmodifiable(
|
||||
ActionButtonBuilder.defaultQuickActionOrder,
|
||||
);
|
||||
|
||||
List<ActionButtonType> get() {
|
||||
return _repository.get();
|
||||
}
|
||||
|
||||
Future<void> set(List<ActionButtonType> order) async {
|
||||
final normalized = _normalizeQuickActionOrder(order);
|
||||
await _repository.set(normalized);
|
||||
}
|
||||
|
||||
Stream<List<ActionButtonType>> watch() {
|
||||
return _repository.watch();
|
||||
}
|
||||
|
||||
List<ActionButtonType> _normalizeQuickActionOrder(List<ActionButtonType> order) {
|
||||
final ordered = <ActionButtonType>{};
|
||||
|
||||
for (final type in order) {
|
||||
if (_quickActionSet.contains(type)) {
|
||||
ordered.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
ordered.addAll(ActionButtonBuilder.defaultQuickActionOrder);
|
||||
|
||||
return ordered.toList(growable: false);
|
||||
}
|
||||
|
||||
List<ActionButtonType> buildQuickActionTypes(
|
||||
ActionButtonContext context, {
|
||||
List<ActionButtonType>? quickActionOrder,
|
||||
int limit = ActionButtonBuilder.defaultQuickActionLimit,
|
||||
}) {
|
||||
final normalized = _normalizeQuickActionOrder(
|
||||
quickActionOrder == null || quickActionOrder.isEmpty
|
||||
? ActionButtonBuilder.defaultQuickActionOrder
|
||||
: quickActionOrder,
|
||||
);
|
||||
|
||||
final seen = <ActionButtonType>{};
|
||||
final result = <ActionButtonType>[];
|
||||
|
||||
for (final type in normalized) {
|
||||
if (!_quickActionSet.contains(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final resolved = _resolveQuickActionType(type, context);
|
||||
if (!seen.add(resolved) || !resolved.shouldShow(context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(resolved);
|
||||
if (result.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Resolve quick action type based on context (e.g., archive -> unarchive)
|
||||
ActionButtonType _resolveQuickActionType(ActionButtonType type, ActionButtonContext context) {
|
||||
if (type == ActionButtonType.archive && context.isArchived) {
|
||||
return ActionButtonType.unarchive;
|
||||
}
|
||||
|
||||
if (type == ActionButtonType.delete && context.asset.isLocalOnly) {
|
||||
return ActionButtonType.deleteLocal;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
class ActionButtonOrderRepository {
|
||||
const ActionButtonOrderRepository();
|
||||
|
||||
static const storeKey = StoreKey.viewerQuickActionOrder;
|
||||
|
||||
List<ActionButtonType> get() {
|
||||
final json = Store.tryGet(storeKey);
|
||||
if (json == null || json.isEmpty) {
|
||||
return ActionButtonBuilder.defaultQuickActionOrder;
|
||||
}
|
||||
|
||||
final deserialized = _deserialize(json);
|
||||
return deserialized.isEmpty ? ActionButtonBuilder.defaultQuickActionOrder : deserialized;
|
||||
}
|
||||
|
||||
Future<void> set(List<ActionButtonType> order) async {
|
||||
final json = _serialize(order);
|
||||
await Store.put(storeKey, json);
|
||||
}
|
||||
|
||||
Stream<List<ActionButtonType>> watch() {
|
||||
return Store.watch(storeKey).map((json) {
|
||||
if (json == null || json.isEmpty) {
|
||||
return ActionButtonBuilder.defaultQuickActionOrder;
|
||||
}
|
||||
final deserialized = _deserialize(json);
|
||||
return deserialized.isEmpty ? ActionButtonBuilder.defaultQuickActionOrder : deserialized;
|
||||
});
|
||||
}
|
||||
|
||||
String _serialize(List<ActionButtonType> order) {
|
||||
return jsonEncode(order.map((type) => type.name).toList());
|
||||
}
|
||||
|
||||
List<ActionButtonType> _deserialize(String json) {
|
||||
try {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
return list
|
||||
.whereType<String>()
|
||||
.map((name) {
|
||||
try {
|
||||
return ActionButtonType.values.byName(name);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<ActionButtonType>()
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final _features = [
|
||||
_Feature(
|
||||
name: 'Main Timeline',
|
||||
icon: Icons.timeline_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Selection Mode Timeline',
|
||||
icon: Icons.developer_mode_rounded,
|
||||
onTap: (ctx, ref) async {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
if (user == null) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
final assets = await ref.read(remoteAssetRepositoryProvider).getSome(user.id);
|
||||
|
||||
final selectedAssets = await ctx.pushRoute<Set<BaseAsset>>(
|
||||
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: assets.toSet()),
|
||||
);
|
||||
|
||||
Logger("FeaturesInDevelopment").fine("Selected ${selectedAssets?.length ?? 0} assets");
|
||||
|
||||
return Future.value();
|
||||
},
|
||||
),
|
||||
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
|
||||
_Feature(
|
||||
name: 'Sync Local',
|
||||
icon: Icons.photo_album_rounded,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Sync Local Full (1)',
|
||||
icon: Icons.photo_library_rounded,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Hash Local Assets (2)',
|
||||
icon: Icons.numbers_outlined,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Sync Remote (3)',
|
||||
icon: Icons.refresh_rounded,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'WAL Checkpoint',
|
||||
icon: Icons.save_rounded,
|
||||
onTap: (_, ref) => ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"),
|
||||
),
|
||||
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
|
||||
_Feature(
|
||||
name: 'Clear Delta Checkpoint',
|
||||
icon: Icons.delete_rounded,
|
||||
onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Clear Local Data',
|
||||
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
|
||||
icon: Icons.delete_forever_rounded,
|
||||
onTap: (_, ref) async {
|
||||
final db = ref.read(driftProvider);
|
||||
await db.localAssetEntity.deleteAll();
|
||||
await db.localAlbumEntity.deleteAll();
|
||||
await db.localAlbumAssetEntity.deleteAll();
|
||||
},
|
||||
),
|
||||
_Feature(
|
||||
name: 'Clear Remote Data',
|
||||
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
|
||||
icon: Icons.delete_sweep_rounded,
|
||||
onTap: (_, ref) async {
|
||||
final db = ref.read(driftProvider);
|
||||
await db.remoteAssetEntity.deleteAll();
|
||||
await db.remoteExifEntity.deleteAll();
|
||||
await db.remoteAlbumEntity.deleteAll();
|
||||
await db.remoteAlbumUserEntity.deleteAll();
|
||||
await db.remoteAlbumAssetEntity.deleteAll();
|
||||
await db.memoryEntity.deleteAll();
|
||||
await db.memoryAssetEntity.deleteAll();
|
||||
await db.stackEntity.deleteAll();
|
||||
await db.personEntity.deleteAll();
|
||||
await db.assetFaceEntity.deleteAll();
|
||||
},
|
||||
),
|
||||
_Feature(
|
||||
name: 'Local Media Summary',
|
||||
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
|
||||
icon: Icons.table_chart_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Remote Media Summary',
|
||||
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
|
||||
icon: Icons.summarize_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Reset Sqlite',
|
||||
icon: Icons.table_view_rounded,
|
||||
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
onTap: (_, ref) async {
|
||||
final drift = ref.read(driftProvider);
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
final migrator = drift.createMigrator();
|
||||
for (final entity in drift.allSchemaEntities) {
|
||||
await migrator.drop(entity);
|
||||
await migrator.create(entity);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
class FeatInDevPage extends StatelessWidget {
|
||||
const FeatInDevPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('features_in_development'.tr()), centerTitle: true),
|
||||
body: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final feat = _features[index];
|
||||
return Consumer(
|
||||
builder: (ctx, ref, _) => ListTile(
|
||||
title: Text(feat.name, style: feat.style),
|
||||
trailing: Icon(feat.icon),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onTap: () => unawaited(feat.onTap(ctx, ref)),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: _features.length,
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Feature {
|
||||
const _Feature({required this.name, required this.icon, required this.onTap, this.style});
|
||||
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final TextStyle? style;
|
||||
final Future<void> Function(BuildContext, WidgetRef _) onTap;
|
||||
}
|
||||
51
mobile/lib/presentation/pages/dev/ui_showcase.page.dart
Normal file
51
mobile/lib/presentation/pages/dev/ui_showcase.page.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
List<Widget> _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) {
|
||||
final children = <Widget>[];
|
||||
|
||||
final items = [
|
||||
(variant: ImmichVariant.filled, title: "Filled Variant"),
|
||||
(variant: ImmichVariant.ghost, title: "Ghost Variant"),
|
||||
];
|
||||
|
||||
for (final (:variant, :title) in items) {
|
||||
children.add(Text(title));
|
||||
children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)]));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class ImmichUIShowcasePage extends StatelessWidget {
|
||||
const ImmichUIShowcasePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Immich UI Showcase')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("IconButton", style: context.textTheme.titleLarge),
|
||||
..._showcaseBuilder(
|
||||
(variant, color) =>
|
||||
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onTap: () {}),
|
||||
),
|
||||
Text("CloseButton", style: context.textTheme.titleLarge),
|
||||
..._showcaseBuilder((variant, color) => ImmichCloseButton(color: color, variant: variant, onTap: () {})),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
/// A widget for cropping an image.
|
||||
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
|
||||
@@ -30,11 +31,13 @@ class DriftCropImagePage extends HookWidget {
|
||||
appBar: AppBar(
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
title: Text("crop".tr()),
|
||||
leading: CloseButton(color: context.primaryColor),
|
||||
leading: const ImmichCloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
|
||||
onPressed: () async {
|
||||
ImmichIconButton(
|
||||
icon: Icons.done_rounded,
|
||||
color: ImmichColor.primary,
|
||||
variant: ImmichVariant.ghost,
|
||||
onTap: () async {
|
||||
final croppedImage = await cropController.croppedImage();
|
||||
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
|
||||
},
|
||||
@@ -72,17 +75,17 @@ class DriftCropImagePage extends HookWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color),
|
||||
onPressed: () {
|
||||
cropController.rotateLeft();
|
||||
},
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_left,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onTap: () => cropController.rotateLeft(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color),
|
||||
onPressed: () {
|
||||
cropController.rotateRight();
|
||||
},
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_right,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onTap: () => cropController.rotateRight(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -109,7 +109,10 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<Widget> slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))];
|
||||
final List<Widget> slivers = [
|
||||
const CreateAlbumButton(),
|
||||
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)),
|
||||
];
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -153,6 +156,9 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart';
|
||||
|
||||
class ReorderButtonsActionButton extends ConsumerWidget {
|
||||
const ReorderButtonsActionButton({super.key, this.originalTheme});
|
||||
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
label: 'reorder_buttons'.tr(),
|
||||
iconData: Icons.swap_vert,
|
||||
iconColor: originalTheme?.iconTheme.color,
|
||||
menuItem: true,
|
||||
onPressed: () async {
|
||||
final viewerNotifier = ref.read(assetViewerProvider.notifier);
|
||||
viewerNotifier.setBottomSheet(true);
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
builder: (sheetContext) => const FractionallySizedBox(heightFactor: 0.75, child: QuickActionConfigurator()),
|
||||
).whenComplete(() {
|
||||
viewerNotifier.setBottomSheet(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
@@ -766,3 +768,68 @@ class AddToAlbumHeader extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateAlbumButton extends ConsumerWidget {
|
||||
const CreateAlbumButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> onCreateAlbum() async {
|
||||
var albumName = await showDialog<String?>(context: context, builder: (context) => const NewAlbumNameModal());
|
||||
if (albumName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final asset = ref.read(currentAssetNotifier);
|
||||
|
||||
if (asset == null) {
|
||||
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
final album = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.createAlbum(title: albumName, assetIds: [asset.remoteId!]);
|
||||
|
||||
if (album == null) {
|
||||
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
|
||||
return;
|
||||
}
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
ref.invalidate(albumsContainingAssetProvider(asset.remoteId!));
|
||||
|
||||
context.pop();
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("add_to_album", style: context.textTheme.titleSmall).tr(),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: const Size(0, 0),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: onCreateAlbum,
|
||||
icon: Icon(Icons.add, color: context.primaryColor),
|
||||
label: Text(
|
||||
"common_create_new_album",
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class NewAlbumNameModal extends StatefulWidget {
|
||||
const NewAlbumNameModal({super.key});
|
||||
|
||||
@override
|
||||
State<NewAlbumNameModal> createState() => _NewAlbumNameModalState();
|
||||
}
|
||||
|
||||
class _NewAlbumNameModalState extends State<NewAlbumNameModal> {
|
||||
TextEditingController nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("album_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
||||
content: SingleChildScrollView(
|
||||
child: TextFormField(
|
||||
controller: nameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(null),
|
||||
child: Text(
|
||||
"cancel",
|
||||
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop(nameController.text.trim());
|
||||
},
|
||||
child: Text(
|
||||
"create_album",
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/viewer_quick_action_order.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||
|
||||
class ViewerBottomBar extends ConsumerWidget {
|
||||
@@ -34,53 +33,25 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final isTrashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final quickActionOrder = ref.watch(viewerQuickActionOrderProvider);
|
||||
|
||||
if (!showControls) {
|
||||
opacity = 0;
|
||||
}
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: isOwner,
|
||||
isArchived: isArchived,
|
||||
isTrashEnabled: isTrashEnabled,
|
||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: currentAlbum,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final quickActionService = ref.watch(quickActionServiceProvider);
|
||||
final quickActionTypes = quickActionService.buildQuickActionTypes(
|
||||
buttonContext,
|
||||
quickActionOrder: quickActionOrder,
|
||||
);
|
||||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
|
||||
Future<void> openConfigurator() async {
|
||||
final viewerNotifier = ref.read(assetViewerProvider.notifier);
|
||||
|
||||
viewerNotifier.setBottomSheet(true);
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
builder: (sheetContext) => const FractionallySizedBox(heightFactor: 0.75, child: QuickActionConfigurator()),
|
||||
).whenComplete(() {
|
||||
viewerNotifier.setBottomSheet(false);
|
||||
});
|
||||
}
|
||||
|
||||
final actions = quickActionTypes
|
||||
.map((type) => type.buildButton(buttonContext))
|
||||
.map((widget) => GestureDetector(onLongPress: openConfigurator, child: widget))
|
||||
.toList(growable: false);
|
||||
if (isOwner) ...[
|
||||
asset.isLocalOnly
|
||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
],
|
||||
];
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: opacity < 255,
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/viewer_quick_action_order.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/action_button_visuals.dart';
|
||||
import 'package:immich_mobile/widgets/common/reorderable_drag_drop_grid.dart';
|
||||
|
||||
class QuickActionConfigurator extends ConsumerStatefulWidget {
|
||||
const QuickActionConfigurator({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<QuickActionConfigurator> createState() => _QuickActionConfiguratorState();
|
||||
}
|
||||
|
||||
class _QuickActionConfiguratorState extends ConsumerState<QuickActionConfigurator> {
|
||||
late List<ActionButtonType> _order;
|
||||
late final ScrollController _scrollController;
|
||||
bool _hasLocalChanges = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_order = List<ActionButtonType>.from(ref.read(viewerQuickActionOrderProvider));
|
||||
_scrollController = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
final item = _order.removeAt(oldIndex);
|
||||
_order.insert(newIndex, item);
|
||||
_hasLocalChanges = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _resetToDefault() {
|
||||
setState(() {
|
||||
_order = List<ActionButtonType>.from(ActionButtonBuilder.defaultQuickActionOrder);
|
||||
_hasLocalChanges = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _cancel() => Navigator.of(context).pop();
|
||||
|
||||
Future<void> _save() async {
|
||||
await ref.read(viewerQuickActionOrderProvider.notifier).setOrder(_order);
|
||||
_hasLocalChanges = false;
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const crossAxisCount = 4;
|
||||
const crossAxisSpacing = 12.0;
|
||||
const mainAxisSpacing = 12.0;
|
||||
const tileHeight = 130.0;
|
||||
final currentOrder = ref.watch(viewerQuickActionOrderProvider);
|
||||
if (!_hasLocalChanges && !listEquals(_order, currentOrder)) {
|
||||
_order = List<ActionButtonType>.from(currentOrder);
|
||||
}
|
||||
|
||||
final hasChanges = !listEquals(currentOrder, _order);
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20, top: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.25),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(2)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('quick_actions_settings_title'.tr(), style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'quick_actions_settings_description'.tr(
|
||||
namedArgs: {'count': ActionButtonBuilder.defaultQuickActionLimit.toString()},
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final rows = (_order.length / crossAxisCount).ceil().clamp(1, 4);
|
||||
final naturalHeight = rows * tileHeight + (rows - 1) * mainAxisSpacing;
|
||||
final shouldScroll = naturalHeight > constraints.maxHeight;
|
||||
final horizontalPadding = 8.0;
|
||||
final tileWidth =
|
||||
(constraints.maxWidth - horizontalPadding - (crossAxisSpacing * (crossAxisCount - 1))) /
|
||||
crossAxisCount;
|
||||
final childAspectRatio = tileWidth / tileHeight;
|
||||
final gridController = shouldScroll ? _scrollController : null;
|
||||
|
||||
return ReorderableDragDropGrid(
|
||||
scrollController: gridController,
|
||||
itemCount: _order.length,
|
||||
itemBuilder: (context, index) {
|
||||
final type = _order[index];
|
||||
return _QuickActionTile(index: index, type: type);
|
||||
},
|
||||
onReorder: _onReorder,
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
childAspectRatio: childAspectRatio,
|
||||
shouldScroll: shouldScroll,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(onPressed: _resetToDefault, child: const Text('reset').tr()),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(onPressed: _cancel, child: const Text('cancel').tr()),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(onPressed: hasChanges ? _save : null, child: const Text('done').tr()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickActionTile extends StatelessWidget {
|
||||
final int index;
|
||||
final ActionButtonType type;
|
||||
|
||||
const _QuickActionTile({required this.index, required this.type});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final borderColor = theme.dividerColor;
|
||||
final backgroundColor = theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.2);
|
||||
final indicatorColor = theme.colorScheme.primary;
|
||||
final accentColor = theme.colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
border: Border.all(color: borderColor),
|
||||
color: backgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: indicatorColor.withValues(alpha: 0.15),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: theme.textTheme.labelSmall?.copyWith(color: indicatorColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(Icons.drag_indicator_rounded, size: 18, color: indicatorColor),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Icon(type.iconData, size: 28, color: theme.colorScheme.onSurface),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(
|
||||
type.localizedLabel(context),
|
||||
style: theme.textTheme.labelSmall?.copyWith(color: accentColor),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
|
||||
@@ -104,7 +105,12 @@ class NativeVideoViewer extends HookConsumerWidget {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
|
||||
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file);
|
||||
// Pass a file:// URI so Android's Uri.parse doesn't
|
||||
// interpret characters like '#' as fragment identifiers.
|
||||
final source = await VideoSource.init(
|
||||
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/share_intent_service.dart';
|
||||
import 'package:immich_mobile/services/upload.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
||||
@@ -25,6 +26,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
final AppRouter router;
|
||||
final UploadService _uploadService;
|
||||
final ShareIntentService _shareIntentService;
|
||||
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
|
||||
|
||||
ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) {
|
||||
_uploadService.taskStatusStream.listen(_updateUploadStatus);
|
||||
@@ -86,6 +88,8 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
for (final attachment in state)
|
||||
if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment,
|
||||
];
|
||||
|
||||
_logger.fine("Upload failed for asset: ${task.task.filename}, exception: ${task.exception}");
|
||||
}
|
||||
|
||||
void _taskProgressCallback(TaskProgressUpdate update) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
final actionButtonOrderRepositoryProvider = Provider<ActionButtonOrderRepository>(
|
||||
(ref) => const ActionButtonOrderRepository(),
|
||||
);
|
||||
|
||||
final quickActionServiceProvider = Provider<QuickActionService>(
|
||||
(ref) => QuickActionService(ref.watch(actionButtonOrderRepositoryProvider)),
|
||||
);
|
||||
|
||||
final viewerQuickActionOrderProvider = StateNotifierProvider<ViewerQuickActionOrderNotifier, List<ActionButtonType>>(
|
||||
(ref) => ViewerQuickActionOrderNotifier(ref.watch(quickActionServiceProvider)),
|
||||
);
|
||||
|
||||
class ViewerQuickActionOrderNotifier extends StateNotifier<List<ActionButtonType>> {
|
||||
final QuickActionService _service;
|
||||
StreamSubscription<List<ActionButtonType>>? _subscription;
|
||||
|
||||
ViewerQuickActionOrderNotifier(this._service) : super(_service.get()) {
|
||||
_subscription = _service.watch().listen((order) {
|
||||
state = order;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> setOrder(List<ActionButtonType> order) async {
|
||||
if (listEquals(state, order)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final previous = state;
|
||||
state = order;
|
||||
|
||||
try {
|
||||
await _service.set(order);
|
||||
} catch (error) {
|
||||
state = previous;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,9 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||
import 'package:immich_mobile/pages/settings/sync_status.page.dart';
|
||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.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/ui_showcase.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/download_info.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||
@@ -286,7 +286,6 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
|
||||
AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: FeatInDevRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftBackupRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
@@ -338,6 +337,7 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: ImmichUIShowcaseRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
// required to handle all deeplinks in deep_link.service.dart
|
||||
// auto_route_library#1722
|
||||
RedirectRoute(path: '*', redirectTo: '/'),
|
||||
|
||||
@@ -1648,22 +1648,6 @@ class FavoritesRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [FeatInDevPage]
|
||||
class FeatInDevRoute extends PageRouteInfo<void> {
|
||||
const FeatInDevRoute({List<PageRouteInfo>? children})
|
||||
: super(FeatInDevRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'FeatInDevRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const FeatInDevPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [FilterImagePage]
|
||||
class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
|
||||
@@ -1831,6 +1815,22 @@ class HeaderSettingsRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [ImmichUIShowcasePage]
|
||||
class ImmichUIShowcaseRoute extends PageRouteInfo<void> {
|
||||
const ImmichUIShowcaseRoute({List<PageRouteInfo>? children})
|
||||
: super(ImmichUIShowcaseRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ImmichUIShowcaseRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const ImmichUIShowcasePage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryPage]
|
||||
class LibraryRoute extends PageRouteInfo<void> {
|
||||
|
||||
@@ -65,7 +65,6 @@ enum AppSettingsEnum<T> {
|
||||
|
||||
class AppSettingsService {
|
||||
const AppSettingsService();
|
||||
|
||||
T getSetting<T>(AppSettingsEnum<T> setting) {
|
||||
return Store.get(setting.storeKey, setting.defaultValue);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
@@ -18,7 +17,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||
@@ -30,7 +28,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/reorder_buttons_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class ActionButtonContext {
|
||||
@@ -60,8 +57,6 @@ class ActionButtonContext {
|
||||
enum ActionButtonType {
|
||||
advancedInfo,
|
||||
share,
|
||||
edit,
|
||||
add,
|
||||
shareLink,
|
||||
similarPhotos,
|
||||
archive,
|
||||
@@ -78,16 +73,10 @@ enum ActionButtonType {
|
||||
unstack,
|
||||
likeActivity;
|
||||
|
||||
String toJson() => name;
|
||||
|
||||
bool shouldShow(ActionButtonContext context) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
|
||||
ActionButtonType.share => true,
|
||||
ActionButtonType.edit =>
|
||||
!context.isInLockedView && //
|
||||
context.asset.isImage,
|
||||
ActionButtonType.add => context.asset.hasRemote,
|
||||
ActionButtonType.shareLink =>
|
||||
!context.isInLockedView && //
|
||||
context.asset.hasRemote,
|
||||
@@ -156,8 +145,6 @@ enum ActionButtonType {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source),
|
||||
ActionButtonType.edit => const EditImageActionButton(),
|
||||
ActionButtonType.add => const AddActionButton(),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
|
||||
ActionButtonType.archive => ArchiveActionButton(source: context.source),
|
||||
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
|
||||
@@ -183,19 +170,6 @@ enum ActionButtonType {
|
||||
class ActionButtonBuilder {
|
||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||
|
||||
static const int defaultQuickActionLimit = 4;
|
||||
|
||||
static const List<ActionButtonType> defaultQuickActionOrder = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.add,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.removeFromAlbum,
|
||||
ActionButtonType.likeActivity,
|
||||
];
|
||||
|
||||
static List<Widget> build(ActionButtonContext context) {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
@@ -220,7 +194,6 @@ class ViewerKebabMenuButtonContext {
|
||||
enum ViewerKebabMenuButtonType {
|
||||
openInfo,
|
||||
viewInTimeline,
|
||||
reorderButtons,
|
||||
cast,
|
||||
download;
|
||||
|
||||
@@ -230,7 +203,6 @@ enum ViewerKebabMenuButtonType {
|
||||
int get group => switch (this) {
|
||||
ViewerKebabMenuButtonType.openInfo => 0,
|
||||
ViewerKebabMenuButtonType.viewInTimeline => 1,
|
||||
ViewerKebabMenuButtonType.reorderButtons => 1,
|
||||
ViewerKebabMenuButtonType.cast => 1,
|
||||
ViewerKebabMenuButtonType.download => 1,
|
||||
};
|
||||
@@ -247,7 +219,6 @@ enum ViewerKebabMenuButtonType {
|
||||
context.isOwner,
|
||||
ViewerKebabMenuButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||
ViewerKebabMenuButtonType.download => context.asset.isRemoteOnly,
|
||||
ViewerKebabMenuButtonType.reorderButtons => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -274,7 +245,6 @@ enum ViewerKebabMenuButtonType {
|
||||
),
|
||||
ViewerKebabMenuButtonType.cast => const CastActionButton(menuItem: true),
|
||||
ViewerKebabMenuButtonType.download => const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
ViewerKebabMenuButtonType.reorderButtons => ReorderButtonsActionButton(originalTheme: context.originalTheme),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
extension ActionButtonTypeVisuals on ActionButtonType {
|
||||
IconData get iconData {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => Icons.help_outline_rounded,
|
||||
ActionButtonType.share => Icons.share_rounded,
|
||||
ActionButtonType.edit => Icons.tune,
|
||||
ActionButtonType.add => Icons.add,
|
||||
ActionButtonType.shareLink => Icons.link_rounded,
|
||||
ActionButtonType.similarPhotos => Icons.compare,
|
||||
ActionButtonType.archive => Icons.archive_outlined,
|
||||
ActionButtonType.unarchive => Icons.unarchive_outlined,
|
||||
ActionButtonType.download => Icons.download,
|
||||
ActionButtonType.trash => Icons.delete_outline_rounded,
|
||||
ActionButtonType.deletePermanent => Icons.delete_forever,
|
||||
ActionButtonType.delete => Icons.delete_sweep_outlined,
|
||||
ActionButtonType.moveToLockFolder => Icons.lock_outline_rounded,
|
||||
ActionButtonType.removeFromLockFolder => Icons.lock_open_rounded,
|
||||
ActionButtonType.deleteLocal => Icons.no_cell_outlined,
|
||||
ActionButtonType.upload => Icons.backup_outlined,
|
||||
ActionButtonType.removeFromAlbum => Icons.remove_circle_outline,
|
||||
ActionButtonType.unstack => Icons.layers_clear_outlined,
|
||||
ActionButtonType.likeActivity => Icons.favorite_border,
|
||||
};
|
||||
}
|
||||
|
||||
String get _labelKey {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => 'troubleshoot',
|
||||
ActionButtonType.share => 'share',
|
||||
ActionButtonType.edit => 'edit',
|
||||
ActionButtonType.add => 'add_to_bottom_bar',
|
||||
ActionButtonType.shareLink => 'share_link',
|
||||
ActionButtonType.similarPhotos => 'view_similar_photos',
|
||||
ActionButtonType.archive => 'to_archive',
|
||||
ActionButtonType.unarchive => 'unarchive',
|
||||
ActionButtonType.download => 'download',
|
||||
ActionButtonType.trash => 'control_bottom_app_bar_trash_from_immich',
|
||||
ActionButtonType.deletePermanent => 'delete_permanently',
|
||||
ActionButtonType.delete => 'delete',
|
||||
ActionButtonType.moveToLockFolder => 'move_to_locked_folder',
|
||||
ActionButtonType.removeFromLockFolder => 'remove_from_locked_folder',
|
||||
ActionButtonType.deleteLocal => 'control_bottom_app_bar_delete_from_local',
|
||||
ActionButtonType.upload => 'upload',
|
||||
ActionButtonType.removeFromAlbum => 'remove_from_album',
|
||||
ActionButtonType.unstack => 'unstack',
|
||||
ActionButtonType.likeActivity => 'like',
|
||||
};
|
||||
}
|
||||
|
||||
String localizedLabel(BuildContext _) => _labelKey.tr();
|
||||
}
|
||||
@@ -53,16 +53,18 @@ class ServerUpdateNotification extends HookConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
serverInfoState.versionStatus.message,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelLarge,
|
||||
Expanded(
|
||||
child: Text(
|
||||
serverInfoState.versionStatus.message,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
|
||||
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
|
||||
const Spacer(),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: openUpdateLink,
|
||||
style: TextButton.styleFrom(
|
||||
|
||||
@@ -155,8 +155,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||
if (kDebugMode || kProfileMode)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.science_rounded),
|
||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||
icon: const Icon(Icons.palette_rounded),
|
||||
onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
|
||||
),
|
||||
if (isCasting)
|
||||
Padding(
|
||||
|
||||
@@ -74,8 +74,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||
if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.science_rounded),
|
||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||
icon: const Icon(Icons.palette_rounded),
|
||||
onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
|
||||
),
|
||||
if (showUploadButton && !isReadonlyModeEnabled)
|
||||
const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A callback that is called when items are reordered.
|
||||
/// [oldIndex] is the original index of the item being moved.
|
||||
/// [newIndex] is the target index where the item should be moved to.
|
||||
typedef ReorderCallback = void Function(int oldIndex, int newIndex);
|
||||
|
||||
/// A callback that is called during drag to update hover state.
|
||||
/// [draggedIndex] is the index of the item being dragged.
|
||||
/// [targetIndex] is the index of the item being hovered over.
|
||||
typedef DragUpdateCallback = void Function(int draggedIndex, int targetIndex);
|
||||
|
||||
/// A reorderable grid that supports drag and drop reordering with smooth animations.
|
||||
///
|
||||
/// This widget provides a drag-and-drop interface for reordering items in a grid layout.
|
||||
/// Items can be dragged to new positions, and the grid will animate smoothly to reflect
|
||||
/// the new order.
|
||||
///
|
||||
/// Features:
|
||||
/// - Smooth animations during drag and drop
|
||||
/// - Instant snap animation on drop completion
|
||||
/// - Visual feedback during dragging
|
||||
/// - Customizable grid layout parameters
|
||||
class ReorderableDragDropGrid extends StatefulWidget {
|
||||
/// Controller for scrolling the grid.
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// The number of items to display.
|
||||
final int itemCount;
|
||||
|
||||
/// Builder function to create each grid item.
|
||||
final Widget Function(BuildContext context, int index) itemBuilder;
|
||||
|
||||
/// Callback when items are reordered.
|
||||
final ReorderCallback onReorder;
|
||||
|
||||
/// Number of columns in the grid.
|
||||
final int crossAxisCount;
|
||||
|
||||
/// Horizontal spacing between grid items.
|
||||
final double crossAxisSpacing;
|
||||
|
||||
/// Vertical spacing between grid items.
|
||||
final double mainAxisSpacing;
|
||||
|
||||
/// The ratio of width to height for each grid item.
|
||||
final double childAspectRatio;
|
||||
|
||||
/// Whether the grid should be scrollable.
|
||||
final bool shouldScroll;
|
||||
|
||||
/// Scale factor for the dragged item feedback widget.
|
||||
final double feedbackScaleFactor;
|
||||
|
||||
/// Opacity for the dragged item feedback widget.
|
||||
final double feedbackOpacity;
|
||||
|
||||
const ReorderableDragDropGrid({
|
||||
super.key,
|
||||
this.scrollController,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
required this.onReorder,
|
||||
required this.crossAxisCount,
|
||||
required this.crossAxisSpacing,
|
||||
required this.mainAxisSpacing,
|
||||
required this.childAspectRatio,
|
||||
this.shouldScroll = true,
|
||||
this.feedbackScaleFactor = 1.05,
|
||||
this.feedbackOpacity = 0.9,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReorderableDragDropGrid> createState() => _ReorderableDragDropGridState();
|
||||
}
|
||||
|
||||
class _ReorderableDragDropGridState extends State<ReorderableDragDropGrid> {
|
||||
int? _draggingIndex;
|
||||
late List<int> _itemOrder;
|
||||
int? _lastHoveredIndex;
|
||||
bool _snapNow = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_itemOrder = List.generate(widget.itemCount, (index) => index);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ReorderableDragDropGrid oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.itemCount != widget.itemCount) {
|
||||
_itemOrder = List.generate(widget.itemCount, (index) => index);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateHover(int draggedIndex, int targetIndex) {
|
||||
if (draggedIndex == targetIndex || _draggingIndex == null) return;
|
||||
|
||||
setState(() {
|
||||
_lastHoveredIndex = targetIndex;
|
||||
final newOrder = List<int>.from(_itemOrder);
|
||||
final draggedOrderIndex = newOrder.indexOf(draggedIndex);
|
||||
final targetOrderIndex = newOrder.indexOf(targetIndex);
|
||||
|
||||
newOrder.removeAt(draggedOrderIndex);
|
||||
newOrder.insert(targetOrderIndex, draggedIndex);
|
||||
_itemOrder = newOrder;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleDragEnd(int draggedIndex, int? targetIndex) {
|
||||
final effectiveTargetIndex =
|
||||
targetIndex ??
|
||||
(() {
|
||||
final currentVisualIndex = _itemOrder.indexOf(draggedIndex);
|
||||
if (currentVisualIndex != draggedIndex) {
|
||||
return _itemOrder[currentVisualIndex];
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
if (effectiveTargetIndex != null && draggedIndex != effectiveTargetIndex) {
|
||||
widget.onReorder(draggedIndex, effectiveTargetIndex);
|
||||
}
|
||||
|
||||
_armSnapNow();
|
||||
|
||||
setState(() {
|
||||
_draggingIndex = null;
|
||||
_lastHoveredIndex = null;
|
||||
_itemOrder = List.generate(widget.itemCount, (i) => i);
|
||||
});
|
||||
}
|
||||
|
||||
void _armSnapNow() {
|
||||
setState(() => _snapNow = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _snapNow = false);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final tileWidth =
|
||||
(constraints.maxWidth - (widget.crossAxisSpacing * (widget.crossAxisCount - 1))) / widget.crossAxisCount;
|
||||
final tileHeight = tileWidth / widget.childAspectRatio;
|
||||
final rows = (_itemOrder.length / widget.crossAxisCount).ceil();
|
||||
final totalHeight = rows * tileHeight + (rows - 1) * widget.mainAxisSpacing;
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: widget.scrollController,
|
||||
physics: widget.shouldScroll ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
height: totalHeight,
|
||||
child: Stack(
|
||||
children: List.generate(widget.itemCount, (index) {
|
||||
final visualIndex = _itemOrder.indexOf(index);
|
||||
final isDragging = _draggingIndex == index;
|
||||
|
||||
final row = visualIndex ~/ widget.crossAxisCount;
|
||||
final col = visualIndex % widget.crossAxisCount;
|
||||
final left = col * (tileWidth + widget.crossAxisSpacing);
|
||||
final top = row * (tileHeight + widget.mainAxisSpacing);
|
||||
|
||||
return _AnimatedGridItem(
|
||||
key: ValueKey(index),
|
||||
index: index,
|
||||
isDragging: isDragging,
|
||||
snapNow: _snapNow,
|
||||
tileWidth: tileWidth,
|
||||
tileHeight: tileHeight,
|
||||
left: left,
|
||||
top: top,
|
||||
feedbackScaleFactor: widget.feedbackScaleFactor,
|
||||
feedbackOpacity: widget.feedbackOpacity,
|
||||
onDragStarted: () {
|
||||
setState(() {
|
||||
_draggingIndex = index;
|
||||
_lastHoveredIndex = index;
|
||||
});
|
||||
},
|
||||
onDragUpdate: (draggedIndex, targetIndex) {
|
||||
_updateHover(draggedIndex, targetIndex);
|
||||
},
|
||||
onDragCompleted: (draggedIndex) {
|
||||
_handleDragEnd(draggedIndex, _lastHoveredIndex);
|
||||
},
|
||||
child: widget.itemBuilder(context, index),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedGridItem extends StatelessWidget {
|
||||
final int index;
|
||||
final bool isDragging;
|
||||
final bool snapNow;
|
||||
final double tileWidth;
|
||||
final double tileHeight;
|
||||
final double left;
|
||||
final double top;
|
||||
final double feedbackScaleFactor;
|
||||
final double feedbackOpacity;
|
||||
final VoidCallback onDragStarted;
|
||||
final DragUpdateCallback onDragUpdate;
|
||||
final Function(int draggedIndex) onDragCompleted;
|
||||
final Widget child;
|
||||
|
||||
const _AnimatedGridItem({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.isDragging,
|
||||
required this.snapNow,
|
||||
required this.tileWidth,
|
||||
required this.tileHeight,
|
||||
required this.left,
|
||||
required this.top,
|
||||
required this.feedbackScaleFactor,
|
||||
required this.feedbackOpacity,
|
||||
required this.onDragStarted,
|
||||
required this.onDragUpdate,
|
||||
required this.onDragCompleted,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Duration animDuration = snapNow ? Duration.zero : const Duration(milliseconds: 150);
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: animDuration,
|
||||
curve: Curves.easeInOut,
|
||||
left: left,
|
||||
top: top,
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
child: DragTarget<int>(
|
||||
onWillAcceptWithDetails: (details) {
|
||||
if (details.data != index) {
|
||||
onDragUpdate(details.data, index);
|
||||
}
|
||||
return details.data != index;
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
Widget displayChild = child;
|
||||
|
||||
if (isDragging) {
|
||||
displayChild = Opacity(opacity: 0.0, child: child);
|
||||
}
|
||||
|
||||
return Draggable<int>(
|
||||
data: index,
|
||||
feedback: Material(
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
child: Opacity(
|
||||
opacity: feedbackOpacity,
|
||||
child: Transform.scale(scale: feedbackScaleFactor, child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
childWhenDragging: const SizedBox.shrink(),
|
||||
onDragStarted: onDragStarted,
|
||||
onDragCompleted: () {
|
||||
onDragCompleted(index);
|
||||
},
|
||||
onDraggableCanceled: (_, __) {
|
||||
onDragCompleted(index);
|
||||
},
|
||||
child: displayChild,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
3
mobile/packages/ui/lib/immich_ui.dart
Normal file
3
mobile/packages/ui/lib/immich_ui.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
export 'src/buttons/close_button.dart';
|
||||
export 'src/buttons/icon_button.dart';
|
||||
export 'src/types.dart';
|
||||
25
mobile/packages/ui/lib/src/buttons/close_button.dart
Normal file
25
mobile/packages/ui/lib/src/buttons/close_button.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/buttons/icon_button.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichCloseButton extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichCloseButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.ghost,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImmichIconButton(
|
||||
key: key,
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onTap: onTap ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
48
mobile/packages/ui/lib/src/buttons/icon_button.dart
Normal file
48
mobile/packages/ui/lib/src/buttons/icon_button.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.filled,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final background = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.onPrimary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: onTap,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
9
mobile/packages/ui/lib/src/types.dart
Normal file
9
mobile/packages/ui/lib/src/types.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
enum ImmichVariant {
|
||||
filled,
|
||||
ghost,
|
||||
}
|
||||
|
||||
enum ImmichColor {
|
||||
primary,
|
||||
secondary,
|
||||
}
|
||||
55
mobile/packages/ui/pubspec.lock
Normal file
55
mobile/packages/ui/pubspec.lock
Normal file
@@ -0,0 +1,55 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
12
mobile/packages/ui/pubspec.yaml
Normal file
12
mobile/packages/ui/pubspec.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
name: immich_ui
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
@@ -1015,6 +1015,13 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.0"
|
||||
immich_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
||||
@@ -43,6 +43,8 @@ dependencies:
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.5.0
|
||||
image_picker: ^1.2.0
|
||||
immich_ui:
|
||||
path: './packages/ui'
|
||||
intl: ^0.20.2
|
||||
isar:
|
||||
git:
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
void main() {
|
||||
group('QuickActionService', () {
|
||||
late QuickActionService service;
|
||||
|
||||
setUp(() {
|
||||
// Use repository with default behavior for testing
|
||||
service = const QuickActionService(ActionButtonOrderRepository());
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should respect custom order', () {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
ownerId: 'owner-id',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final customOrder = [
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.delete,
|
||||
];
|
||||
|
||||
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||
|
||||
expect(types.length, lessThanOrEqualTo(ActionButtonBuilder.defaultQuickActionLimit));
|
||||
expect(types.first, ActionButtonType.archive);
|
||||
expect(types[1], ActionButtonType.share);
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should resolve archive to unarchive when archived', () {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
ownerId: 'owner-id',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: true, // archived
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final customOrder = [ActionButtonType.archive];
|
||||
|
||||
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||
|
||||
expect(types.contains(ActionButtonType.unarchive), isTrue);
|
||||
expect(types.contains(ActionButtonType.archive), isFalse);
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should filter types that shouldShow returns false', () {
|
||||
final localAsset = LocalAsset(
|
||||
id: 'local-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: localAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final customOrder = [
|
||||
ActionButtonType.archive, // should not show for local-only asset
|
||||
ActionButtonType.share,
|
||||
];
|
||||
|
||||
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||
|
||||
expect(types.contains(ActionButtonType.archive), isFalse);
|
||||
expect(types.contains(ActionButtonType.share), isTrue);
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should respect limit', () {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
ownerId: 'owner-id',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final types = service.buildQuickActionTypes(
|
||||
context,
|
||||
quickActionOrder: ActionButtonBuilder.defaultQuickActionOrder,
|
||||
limit: 2,
|
||||
);
|
||||
|
||||
expect(types.length, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,11 +3,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
LocalAsset createLocalAsset({
|
||||
@@ -142,56 +137,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('edit button', () {
|
||||
test('should show for images when not in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(type: AssetType.image),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.edit.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show in locked view', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(type: AssetType.image),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.edit.shouldShow(context), isFalse);
|
||||
});
|
||||
|
||||
test('should not show for non-image assets', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: createRemoteAsset(type: AssetType.video),
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.edit.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('shareLink button', () {
|
||||
test('should show when not in locked view and asset has remote', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
@@ -1016,38 +961,5 @@ void main() {
|
||||
expect(archivedWidgets, isNotEmpty);
|
||||
expect(nonArchivedWidgets, isNotEmpty);
|
||||
});
|
||||
|
||||
test('should build quick actions honoring custom order', () {
|
||||
final remoteAsset = createRemoteAsset();
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final quickActionService = const QuickActionService(ActionButtonOrderRepository());
|
||||
final quickActionTypes = quickActionService.buildQuickActionTypes(
|
||||
context,
|
||||
quickActionOrder: const [
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.delete,
|
||||
],
|
||||
);
|
||||
|
||||
final quickActions = quickActionTypes.map((type) => type.buildButton(context)).toList();
|
||||
|
||||
expect(quickActions.length, ActionButtonBuilder.defaultQuickActionLimit);
|
||||
expect(quickActions.first, isA<ArchiveActionButton>());
|
||||
expect(quickActions[1], isA<ShareActionButton>());
|
||||
expect(quickActions[2], isA<EditImageActionButton>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
214
plugins/package-lock.json
generated
214
plugins/package-lock.json
generated
@@ -15,9 +15,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
|
||||
"integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
|
||||
"integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz",
|
||||
"integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
|
||||
"integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -49,9 +49,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -66,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -83,9 +83,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -117,9 +117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -134,9 +134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -151,9 +151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz",
|
||||
"integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
|
||||
"integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -168,9 +168,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz",
|
||||
"integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
|
||||
"integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -202,9 +202,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz",
|
||||
"integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
|
||||
"integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -219,9 +219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz",
|
||||
"integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
|
||||
"integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -236,9 +236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz",
|
||||
"integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
|
||||
"integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -253,9 +253,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz",
|
||||
"integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
|
||||
"integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -270,9 +270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz",
|
||||
"integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
|
||||
"integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -287,9 +287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -321,9 +321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -338,9 +338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -355,9 +355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -372,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -389,9 +389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -406,9 +406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -423,9 +423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz",
|
||||
"integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
|
||||
"integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -440,9 +440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -467,9 +467,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
|
||||
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
|
||||
"integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -480,32 +480,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.0",
|
||||
"@esbuild/android-arm": "0.27.0",
|
||||
"@esbuild/android-arm64": "0.27.0",
|
||||
"@esbuild/android-x64": "0.27.0",
|
||||
"@esbuild/darwin-arm64": "0.27.0",
|
||||
"@esbuild/darwin-x64": "0.27.0",
|
||||
"@esbuild/freebsd-arm64": "0.27.0",
|
||||
"@esbuild/freebsd-x64": "0.27.0",
|
||||
"@esbuild/linux-arm": "0.27.0",
|
||||
"@esbuild/linux-arm64": "0.27.0",
|
||||
"@esbuild/linux-ia32": "0.27.0",
|
||||
"@esbuild/linux-loong64": "0.27.0",
|
||||
"@esbuild/linux-mips64el": "0.27.0",
|
||||
"@esbuild/linux-ppc64": "0.27.0",
|
||||
"@esbuild/linux-riscv64": "0.27.0",
|
||||
"@esbuild/linux-s390x": "0.27.0",
|
||||
"@esbuild/linux-x64": "0.27.0",
|
||||
"@esbuild/netbsd-arm64": "0.27.0",
|
||||
"@esbuild/netbsd-x64": "0.27.0",
|
||||
"@esbuild/openbsd-arm64": "0.27.0",
|
||||
"@esbuild/openbsd-x64": "0.27.0",
|
||||
"@esbuild/openharmony-arm64": "0.27.0",
|
||||
"@esbuild/sunos-x64": "0.27.0",
|
||||
"@esbuild/win32-arm64": "0.27.0",
|
||||
"@esbuild/win32-ia32": "0.27.0",
|
||||
"@esbuild/win32-x64": "0.27.0"
|
||||
"@esbuild/aix-ppc64": "0.27.1",
|
||||
"@esbuild/android-arm": "0.27.1",
|
||||
"@esbuild/android-arm64": "0.27.1",
|
||||
"@esbuild/android-x64": "0.27.1",
|
||||
"@esbuild/darwin-arm64": "0.27.1",
|
||||
"@esbuild/darwin-x64": "0.27.1",
|
||||
"@esbuild/freebsd-arm64": "0.27.1",
|
||||
"@esbuild/freebsd-x64": "0.27.1",
|
||||
"@esbuild/linux-arm": "0.27.1",
|
||||
"@esbuild/linux-arm64": "0.27.1",
|
||||
"@esbuild/linux-ia32": "0.27.1",
|
||||
"@esbuild/linux-loong64": "0.27.1",
|
||||
"@esbuild/linux-mips64el": "0.27.1",
|
||||
"@esbuild/linux-ppc64": "0.27.1",
|
||||
"@esbuild/linux-riscv64": "0.27.1",
|
||||
"@esbuild/linux-s390x": "0.27.1",
|
||||
"@esbuild/linux-x64": "0.27.1",
|
||||
"@esbuild/netbsd-arm64": "0.27.1",
|
||||
"@esbuild/netbsd-x64": "0.27.1",
|
||||
"@esbuild/openbsd-arm64": "0.27.1",
|
||||
"@esbuild/openbsd-x64": "0.27.1",
|
||||
"@esbuild/openharmony-arm64": "0.27.1",
|
||||
"@esbuild/sunos-x64": "0.27.1",
|
||||
"@esbuild/win32-arm64": "0.27.1",
|
||||
"@esbuild/win32-ia32": "0.27.1",
|
||||
"@esbuild/win32-x64": "0.27.1"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
|
||||
1234
pnpm-lock.yaml
generated
1234
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,7 @@ export class MetadataRepository {
|
||||
// Enable exiftool LFS to parse metadata for files larger than 2GB.
|
||||
readArgs: ['-api', 'largefilesupport=1'],
|
||||
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
|
||||
taskTimeoutMillis: 2 * 60 * 1000,
|
||||
});
|
||||
|
||||
constructor(private logger: LoggingRepository) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"build:stats": "BUILD_STATS=true vite build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "vite preview",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'state_referenced_locally:ignore'",
|
||||
"check:typescript": "tsc --noEmit",
|
||||
"check:watch": "pnpm run check:svelte --watch",
|
||||
"check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript",
|
||||
@@ -98,7 +98,7 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^6.0.0",
|
||||
"svelte": "5.45.2",
|
||||
"svelte": "5.45.5",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
|
||||
@@ -114,7 +114,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await modalManager.show(AssetChangeDateModal, { asset: toTimelineAsset(asset), initialDate: dateTime });
|
||||
await modalManager.show(AssetChangeDateModal, {
|
||||
asset: toTimelineAsset(asset),
|
||||
initialDate: dateTime,
|
||||
initialTimeZone: timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
// the performance benefits of deferred layouts while still supporting deep linking
|
||||
// to assets at the end of the timeline.
|
||||
timelineManager.isScrollingOnLoad = true;
|
||||
const monthGroup = await timelineManager.findMonthGroupForAsset(assetId);
|
||||
const monthGroup = await timelineManager.findMonthGroupForAsset({ id: assetId });
|
||||
if (!monthGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { plainDateTimeCompare, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { AssetOrder } from '@immich/sdk';
|
||||
import { AssetOrder, type AssetResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { MonthGroup } from '../month-group.svelte';
|
||||
import { TimelineManager } from '../timeline-manager.svelte';
|
||||
@@ -7,12 +7,16 @@ import type { AssetDescriptor, Direction, TimelineAsset } from '../types';
|
||||
|
||||
export async function getAssetWithOffset(
|
||||
timelineManager: TimelineManager,
|
||||
assetDescriptor: AssetDescriptor,
|
||||
assetDescriptor: AssetDescriptor | AssetResponseDto,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
direction: Direction,
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
const { asset, monthGroup } = findMonthGroupForAsset(timelineManager, assetDescriptor.id) ?? {};
|
||||
if (!monthGroup || !asset) {
|
||||
const monthGroup = await timelineManager.findMonthGroupForAsset(assetDescriptor);
|
||||
if (!monthGroup) {
|
||||
return;
|
||||
}
|
||||
const asset = monthGroup.findAssetById(assetDescriptor);
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -524,6 +524,7 @@ describe('TimelineManager', () => {
|
||||
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
|
||||
]);
|
||||
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
|
||||
sdkMock.getAssetInfo.mockRejectedValue(new Error('Asset not found'));
|
||||
await timelineManager.updateViewport({ width: 1588, height: 1000 });
|
||||
});
|
||||
|
||||
|
||||
@@ -16,12 +16,13 @@ import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websoc
|
||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||
import {
|
||||
isAssetResponseDto,
|
||||
setDifference,
|
||||
toTimelineAsset,
|
||||
type TimelineDateTime,
|
||||
type TimelineYearMonth,
|
||||
} from '$lib/utils/timeline-util';
|
||||
import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
|
||||
import { AssetOrder, getAssetInfo, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
|
||||
import { clamp, isEqual } from 'lodash-es';
|
||||
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
|
||||
import { DayGroup } from './day-group.svelte';
|
||||
@@ -343,27 +344,30 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
this.addAssetsUpsertSegments([...notExcluded]);
|
||||
}
|
||||
|
||||
async findMonthGroupForAsset(id: string) {
|
||||
async findMonthGroupForAsset(asset: AssetDescriptor | AssetResponseDto) {
|
||||
if (!this.isInitialized) {
|
||||
await this.initTask.waitUntilCompletion();
|
||||
}
|
||||
|
||||
const { id } = asset;
|
||||
let { monthGroup } = findMonthGroupForAssetUtil(this, id) ?? {};
|
||||
if (monthGroup) {
|
||||
return monthGroup;
|
||||
}
|
||||
|
||||
const response = await getAssetInfo({ ...authManager.params, id }).catch(() => null);
|
||||
const response = isAssetResponseDto(asset)
|
||||
? asset
|
||||
: await getAssetInfo({ ...authManager.params, id }).catch(() => null);
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = toTimelineAsset(response);
|
||||
if (!asset || this.isExcluded(asset)) {
|
||||
const timelineAsset = toTimelineAsset(response);
|
||||
if (this.isExcluded(timelineAsset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
monthGroup = await this.#loadMonthGroupAtTime(asset.localDateTime, { cancelable: false });
|
||||
monthGroup = await this.#loadMonthGroupAtTime(timelineAsset.localDateTime, { cancelable: false });
|
||||
if (monthGroup?.findAssetById({ id })) {
|
||||
return monthGroup;
|
||||
}
|
||||
@@ -532,14 +536,14 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
}
|
||||
|
||||
async getLaterAsset(
|
||||
assetDescriptor: AssetDescriptor,
|
||||
assetDescriptor: AssetDescriptor | AssetResponseDto,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
return await getAssetWithOffset(this, assetDescriptor, interval, 'later');
|
||||
}
|
||||
|
||||
async getEarlierAsset(
|
||||
assetDescriptor: AssetDescriptor,
|
||||
assetDescriptor: AssetDescriptor | AssetResponseDto,
|
||||
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||
): Promise<TimelineAsset | undefined> {
|
||||
return await getAssetWithOffset(this, assetDescriptor, interval, 'earlier');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TimelineAsset, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
|
||||
import type { AssetDescriptor, TimelineAsset, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||
@@ -192,8 +192,13 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
|
||||
};
|
||||
};
|
||||
|
||||
export const isTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): unknownAsset is TimelineAsset =>
|
||||
(unknownAsset as TimelineAsset).ratio !== undefined;
|
||||
export const isTimelineAsset = (
|
||||
unknownAsset: AssetDescriptor | AssetResponseDto | TimelineAsset,
|
||||
): unknownAsset is TimelineAsset => (unknownAsset as TimelineAsset).ratio !== undefined;
|
||||
|
||||
export const isAssetResponseDto = (
|
||||
unknownAsset: AssetDescriptor | AssetResponseDto | TimelineAsset,
|
||||
): unknownAsset is AssetResponseDto => (unknownAsset as AssetResponseDto).type !== undefined;
|
||||
|
||||
export const isTimelineAssets = (assets: AssetResponseDto[] | TimelineAsset[]): assets is TimelineAsset[] =>
|
||||
assets.length === 0 || 'ratio' in assets[0];
|
||||
|
||||
@@ -15,9 +15,24 @@
|
||||
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
|
||||
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { mdiDotsVertical, mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
import ChangeDescription from '$lib/components/timeline/actions/ChangeDescriptionAction.svelte';
|
||||
import ChangeLocation from '$lib/components/timeline/actions/ChangeLocationAction.svelte';
|
||||
import CreateSharedLink from '$lib/components/timeline/actions/CreateSharedLinkAction.svelte';
|
||||
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
|
||||
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
|
||||
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
|
||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@@ -79,6 +94,11 @@
|
||||
// navigate to parent
|
||||
await navigateToView(tag.parent ? tag.parent.path : '');
|
||||
};
|
||||
|
||||
const handleSetVisibility = (assetIds: string[]) => {
|
||||
timelineManager.removeAssets(assetIds);
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
@@ -131,3 +151,45 @@
|
||||
{/if}
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
||||
<section>
|
||||
{#if assetInteraction.selectionActive}
|
||||
<div class="fixed top-0 start-0 w-full">
|
||||
<AssetSelectControlBar
|
||||
ownerId={$user.id}
|
||||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => assetInteraction.clearMultiselect()}
|
||||
>
|
||||
<CreateSharedLink />
|
||||
<SelectAllAssets {timelineManager} {assetInteraction} />
|
||||
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))}
|
||||
></FavoriteAction>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeDescription menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction
|
||||
menuItem
|
||||
onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))}
|
||||
/>
|
||||
{#if $preferences.tags.enabled}
|
||||
<TagAction menuItem />
|
||||
{/if}
|
||||
<DeleteAssets
|
||||
menuItem
|
||||
onAssetDelete={(assetIds) => timelineManager.removeAssets(assetIds)}
|
||||
onUndoDelete={(assets) => timelineManager.upsertAssets(assets)}
|
||||
/>
|
||||
<SetVisibilityAction menuItem onVisibilitySet={handleSetVisibility} />
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user