Compare commits
67 Commits
v1.111.0
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f05e9ca9 | ||
|
|
11f41099c3 | ||
|
|
96481aae5d | ||
|
|
4a42a72bd3 | ||
|
|
66f2ac8ce3 | ||
|
|
6b2de807a7 | ||
|
|
96f8050143 | ||
|
|
14689462f8 | ||
|
|
fb68da2b51 | ||
|
|
720b9a286e | ||
|
|
d93ccb1669 | ||
|
|
c34fc4f2d1 | ||
|
|
905a062a6e | ||
|
|
aeed24b5b4 | ||
|
|
28ba22e8c1 | ||
|
|
5b64456f48 | ||
|
|
02fd6d22b3 | ||
|
|
10ed31d725 | ||
|
|
23d4314eed | ||
|
|
ea135cc310 | ||
|
|
745e1b003d | ||
|
|
1dae622dbc | ||
|
|
8ca24f0ef2 | ||
|
|
f679021f0e | ||
|
|
65f5118bdd | ||
|
|
9f4fad2a0f | ||
|
|
325fb4b5d1 | ||
|
|
f040c9fb38 | ||
|
|
0eacdf93eb | ||
|
|
20262209ce | ||
|
|
dd638ac207 | ||
|
|
d5b23373c7 | ||
|
|
9765ccb5a7 | ||
|
|
82d934d09d | ||
|
|
2821e0bf95 | ||
|
|
bb3d9b6306 | ||
|
|
c83df2686a | ||
|
|
94da5942bd | ||
|
|
54d2c12fff | ||
|
|
64fcb25971 | ||
|
|
7f03bd8440 | ||
|
|
2974cdbbee | ||
|
|
f0677735fd | ||
|
|
bb78eb4c4b | ||
|
|
4ed75f2ac9 | ||
|
|
3f4b783889 | ||
|
|
3968d76a57 | ||
|
|
55b31d1ce2 | ||
|
|
37cc6fbf27 | ||
|
|
899b8a0ce7 | ||
|
|
d3a5490e71 | ||
|
|
3afb5b497f | ||
|
|
1f0f880ecb | ||
|
|
2c05ceaf50 | ||
|
|
01f8b7e458 | ||
|
|
b73f7fe16f | ||
|
|
281cfc95a4 | ||
|
|
3a3ea6135e | ||
|
|
c44271e9b2 | ||
|
|
86904a8382 | ||
|
|
cf54829b3b | ||
|
|
990627e00d | ||
|
|
41580696c7 | ||
|
|
2423bb36c4 | ||
|
|
82b899649d | ||
|
|
8ee8450d18 | ||
|
|
6d47d52b3c |
4
.github/workflows/cli.yml
vendored
4
.github/workflows/cli.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.5.0
|
||||
uses: docker/setup-buildx-action@v3.6.1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.5.0
|
||||
uses: docker/build-push-action@v6.6.1
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
4
.github/workflows/docker-cleanup.yml
vendored
4
.github/workflows/docker-cleanup.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.7.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.5.0
|
||||
uses: docker/setup-buildx-action@v3.6.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.5.0
|
||||
uses: docker/build-push-action@v6.6.1
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/dist
|
||||
@@ -1,28 +0,0 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/no-process-exit': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 as core
|
||||
FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 AS core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
60
cli/eslint.config.mjs
Normal file
60
cli/eslint.config.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['eslint.config.mjs', 'dist'],
|
||||
},
|
||||
...compat.extends(
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:unicorn/recommended',
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/no-process-exit': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
1531
cli/package-lock.json
generated
1531
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,30 +13,33 @@
|
||||
"cli"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.14.12",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@vitest/coverage-v8": "^1.2.2",
|
||||
"@types/node": "^20.14.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^12.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"globals": "^15.9.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest-fetch-mock": "^0.3.0",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -46,6 +46,8 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
immich-web:
|
||||
container_name: immich_web
|
||||
@@ -91,6 +93,8 @@ services:
|
||||
depends_on:
|
||||
- database
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
|
||||
@@ -21,6 +21,8 @@ services:
|
||||
- redis
|
||||
- database
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
@@ -40,6 +42,8 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
@@ -67,7 +71,7 @@ services:
|
||||
interval: 5m
|
||||
start_interval: 30s
|
||||
start_period: 5m
|
||||
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
|
||||
command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
|
||||
restart: always
|
||||
|
||||
# set IMMICH_METRICS=true in .env to enable metrics
|
||||
@@ -87,7 +91,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.1.0-ubuntu@sha256:c7fc29ec783d5e7fc1bdfaad6f92345a345cffbc5d21c388ca228175006fc107
|
||||
image: grafana/grafana:11.1.3-ubuntu@sha256:e10453733015f31103cb530425f32c994816b50102886fa885dafea2c50a711c
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ services:
|
||||
- redis
|
||||
- database
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
@@ -41,6 +43,8 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
@@ -65,7 +69,7 @@ services:
|
||||
interval: 5m
|
||||
start_interval: 30s
|
||||
start_period: 5m
|
||||
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
|
||||
command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -294,6 +294,12 @@ You need to enable WebSockets on your reverse proxy.
|
||||
|
||||
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md).
|
||||
|
||||
### How can I reduce the log verbosity of Redis?
|
||||
|
||||
To decrease Redis logs, you can add the following line to the `redis:` section of the `docker-compose.yml`:
|
||||
|
||||
` command: redis-server --loglevel warning`
|
||||
|
||||
### How can I run Immich as a non-root user?
|
||||
|
||||
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
|
||||
|
||||
@@ -145,28 +145,36 @@ const config = {
|
||||
label: 'Installation',
|
||||
to: '/docs/install/requirements',
|
||||
},
|
||||
{
|
||||
label: 'Contributing',
|
||||
to: '/docs/overview/support-the-project',
|
||||
},
|
||||
{
|
||||
label: 'Privacy Policy',
|
||||
to: '/privacy-policy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
title: 'Documentation',
|
||||
items: [
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discord.immich.app',
|
||||
label: 'Roadmap',
|
||||
to: '/roadmap',
|
||||
},
|
||||
{
|
||||
label: 'Reddit',
|
||||
href: 'https://www.reddit.com/r/immich/',
|
||||
label: 'API',
|
||||
to: '/docs/api',
|
||||
},
|
||||
{
|
||||
label: 'Cursed Knowledge',
|
||||
to: '/cursed-knowledge',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
items: [
|
||||
// {
|
||||
// label: "Blog",
|
||||
// to: "/blog",
|
||||
// },
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/immich-app/immich',
|
||||
@@ -175,6 +183,14 @@ const config = {
|
||||
label: 'YouTube',
|
||||
href: 'https://www.youtube.com/@immich-app',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discord.immich.app',
|
||||
},
|
||||
{
|
||||
label: 'Reddit',
|
||||
href: 'https://www.reddit.com/r/immich/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
46
docs/package-lock.json
generated
46
docs/package-lock.json
generated
@@ -12754,9 +12754,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"version": "8.4.40",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
|
||||
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -13600,9 +13600,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -13747,9 +13747,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.12.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz",
|
||||
"integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
@@ -16014,9 +16015,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
|
||||
"integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
||||
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -16376,9 +16377,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -16714,12 +16715,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/url": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz",
|
||||
"integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==",
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
||||
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^1.4.1",
|
||||
"qs": "^6.11.2"
|
||||
"qs": "^6.12.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/url-loader": {
|
||||
@@ -16783,7 +16788,8 @@
|
||||
"node_modules/url/node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.10.4",
|
||||
|
||||
@@ -63,6 +63,11 @@ const projects: CommunityProjectProps[] = [
|
||||
description: 'Powershell Module for the Immich API',
|
||||
url: 'https://github.com/hanpq/PSImmich',
|
||||
},
|
||||
{
|
||||
title: 'Immich Distribution',
|
||||
description: 'Snap package for easy install and zero-care auto updates of Immich. Self-hosted photo management.',
|
||||
url: 'https://immich-distribution.nsg.cc',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js';
|
||||
import {
|
||||
mdiCalendarToday,
|
||||
mdiCrosshairsOff,
|
||||
mdiLeadPencil,
|
||||
mdiLockOff,
|
||||
mdiLockOutline,
|
||||
mdiSpeedometerSlow,
|
||||
mdiWeb,
|
||||
mdiWrap,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
import { Item as TimelineItem, Timeline } from '../components/timeline';
|
||||
@@ -8,6 +17,41 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
|
||||
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiWrap,
|
||||
iconColor: 'gray',
|
||||
title: 'Carriage returns in bash scripts are cursed',
|
||||
description: 'Git can be configured to automatically convert LF to CRLF on checkout and CRLF breaks bash scripts.',
|
||||
link: {
|
||||
url: 'https://github.com/immich-app/immich/pull/11613',
|
||||
text: '#11613',
|
||||
},
|
||||
date: new Date(2024, 7, 7),
|
||||
},
|
||||
{
|
||||
icon: mdiLockOff,
|
||||
iconColor: 'red',
|
||||
title: 'Fetch inside Cloudflare Workers is cursed',
|
||||
description:
|
||||
'Fetch requests in Cloudflare Workers use http by default, even if you explicitly specify https, which can often cause redirect loops.',
|
||||
link: {
|
||||
url: 'https://community.cloudflare.com/t/does-cloudflare-worker-allow-secure-https-connection-to-fetch-even-on-flexible-ssl/68051/5',
|
||||
text: 'Cloudflare',
|
||||
},
|
||||
date: new Date(2024, 7, 7),
|
||||
},
|
||||
{
|
||||
icon: mdiCrosshairsOff,
|
||||
iconColor: 'gray',
|
||||
title: 'GPS sharing on mobile is cursed',
|
||||
description:
|
||||
'Some phones will silently strip GPS data from images when apps without location permission try to access them.',
|
||||
link: {
|
||||
url: 'https://github.com/immich-app/immich/discussions/11268',
|
||||
text: '#11268',
|
||||
},
|
||||
date: new Date(2024, 6, 21),
|
||||
},
|
||||
{
|
||||
icon: mdiLeadPencil,
|
||||
iconColor: 'gold',
|
||||
|
||||
114
docs/src/pages/privacy-policy.tsx
Normal file
114
docs/src/pages/privacy-policy.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Layout from '@theme/Layout';
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
function HomepageHeader() {
|
||||
const { isDarkTheme } = useColorMode();
|
||||
|
||||
return (
|
||||
<header>
|
||||
<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 border border-red-400 rounded-2xl bg-slate-200 dark:bg-immich-dark-gray">
|
||||
<section>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Last updated: July 31st 2024</p>
|
||||
<p>
|
||||
Welcome to Immich. We are committed to respecting your privacy. This Privacy Policy sets out how we collect,
|
||||
use, and share information when you use our Immich app.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 1. Scope of This Policy */}
|
||||
<section>
|
||||
<h2>1. Scope of This Policy</h2>
|
||||
<p>
|
||||
This Privacy Policy applies to the Immich app ("we", "our", or "us") and covers our collection, use, and
|
||||
disclosure of your information. This Policy does not cover any third-party websites, services, or
|
||||
applications that can be accessed through our app, or third-party services you may access through Immich.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 2. Information We Collect */}
|
||||
<section>
|
||||
<h2>2. Information We Collect</h2>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Locally Stored Data</strong>: Immich stores all your photos, albums, settings, and locally on your
|
||||
device. We do not have access to this data, nor do we transmit or store it on any of our servers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<strong>Purchase Information:</strong> When you make a purchase within the{' '}
|
||||
<a href="https://buy.immich.app">https://buy.immich.app</a>, we collect the following information for tax
|
||||
calculation purposes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Country of origin</li>
|
||||
<li>Postal code (if the user is from Canada or the United States)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Use of Your Information */}
|
||||
<section>
|
||||
<h2>3. Use of Your Information</h2>
|
||||
<p>
|
||||
<strong>Tax Calculation:</strong> The country of origin and postal code (for users from Canada or the United
|
||||
States) are collected solely for determining the applicable tax rates on your purchase.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 4. Sharing of Your Information */}
|
||||
<section>
|
||||
<h2>4. Sharing of Your Information</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Tax Authorities:</strong> The purchase information may be shared with tax authorities as required
|
||||
by law.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Payment Providers:</strong> The purchase information may be shared with payment providers where
|
||||
required.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 5. Changes to This Policy */}
|
||||
<section>
|
||||
<h2>5. Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update our Privacy Policy from time to time. If we make any changes, we will notify you by revising
|
||||
the "Last updated" date at the top of this policy. It's encouraged that users frequently check this page for
|
||||
any changes to stay informed about how we are helping to protect the personal information we collect.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 6. Contact Us */}
|
||||
<section>
|
||||
<h2>6. Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about this Privacy Policy, please contact us at{' '}
|
||||
<a href="mailto:immich@futo.org">immich@futo.org</a>
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
return (
|
||||
<Layout
|
||||
title="Home"
|
||||
description="immich Self-hosted photo and video backup solution directly from your mobile phone "
|
||||
noFooter={true}
|
||||
>
|
||||
<HomepageHeader />
|
||||
<div className="flex flex-col place-items-center place-content-center">
|
||||
<p>This project is available under GNU AGPL v3 license.</p>
|
||||
<p className="text-xs">Privacy should not be a luxury</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/prefer-event-target': 'off',
|
||||
'unicorn/no-thenable': 'off',
|
||||
},
|
||||
};
|
||||
64
e2e/eslint.config.mjs
Normal file
64
e2e/eslint.config.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
...compat.extends(
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:unicorn/recommended',
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/prefer-event-target': 'off',
|
||||
'unicorn/no-thenable': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
1737
e2e/package-lock.json
generated
1737
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,23 +19,26 @@
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/node": "^20.14.13",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@vitest/coverage-v8": "^1.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"exiftool-vendored": "^28.0.0",
|
||||
"globals": "^15.9.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
"oidc-provider": "^8.5.1",
|
||||
@@ -47,7 +50,7 @@
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.16.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
|
||||
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
@@ -32,9 +32,6 @@ describe('/search', () => {
|
||||
let assetOneJpg5: AssetMediaResponseDto;
|
||||
let assetSprings: AssetMediaResponseDto;
|
||||
let assetLast: AssetMediaResponseDto;
|
||||
let cities: string[];
|
||||
let states: string[];
|
||||
let countries: string[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
@@ -85,7 +82,7 @@ describe('/search', () => {
|
||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||
const coordinates = [
|
||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||
{ latitude: 63.0695, longitude: -151.0074 }, // denali
|
||||
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
|
||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
|
||||
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
|
||||
@@ -101,16 +98,15 @@ describe('/search', () => {
|
||||
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
|
||||
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
|
||||
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
|
||||
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
|
||||
];
|
||||
|
||||
const updates = assets.map((asset, i) =>
|
||||
updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
const updates = coordinates.map((dto, i) =>
|
||||
updateAsset({ id: assets[i].id, updateAssetDto: dto }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
for (const asset of assets) {
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
||||
for (const [i] of coordinates.entries()) {
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id });
|
||||
}
|
||||
|
||||
[
|
||||
@@ -137,12 +133,6 @@ describe('/search', () => {
|
||||
assetLast = assets.at(-1) as AssetMediaResponseDto;
|
||||
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id);
|
||||
cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort();
|
||||
states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort();
|
||||
countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort();
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -321,23 +311,120 @@ describe('/search', () => {
|
||||
},
|
||||
{
|
||||
should: 'should search by city',
|
||||
deferred: () => ({ dto: { city: 'Accra' }, assets: [assetHeic] }),
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
city: 'Accra',
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetHeic],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: "should search city ('')",
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
city: '',
|
||||
isVisible: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search city (null)',
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
city: null,
|
||||
isVisible: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search by state',
|
||||
deferred: () => ({ dto: { state: 'New York' }, assets: [assetDensity] }),
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
state: 'New York',
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetDensity],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: "should search state ('')",
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
state: '',
|
||||
isVisible: true,
|
||||
withExif: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast, assetNotocactus],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search state (null)',
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
state: null,
|
||||
isVisible: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast, assetNotocactus],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search by country',
|
||||
deferred: () => ({ dto: { country: 'France' }, assets: [assetFalcon] }),
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
country: 'France',
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetFalcon],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: "should search country ('')",
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
country: '',
|
||||
isVisible: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search country (null)',
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
country: null,
|
||||
isVisible: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search by make',
|
||||
deferred: () => ({ dto: { make: 'Canon' }, assets: [assetFalcon, assetDenali] }),
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
make: 'Canon',
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetFalcon, assetDenali],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search by model',
|
||||
deferred: () => ({ dto: { model: 'Canon EOS 7D' }, assets: [assetDenali] }),
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
model: 'Canon EOS 7D',
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetDenali],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should allow searching the upload library (libraryId: null)',
|
||||
@@ -450,32 +537,79 @@ describe('/search', () => {
|
||||
|
||||
it('should get suggestions for country', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=country')
|
||||
.get('/search/suggestions?type=country&includeNull=true')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual(countries);
|
||||
expect(body).toEqual([
|
||||
'Cuba',
|
||||
'France',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Japan',
|
||||
'Morocco',
|
||||
"People's Republic of China",
|
||||
'Russian Federation',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Switzerland',
|
||||
'United States of America',
|
||||
null,
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should get suggestions for state', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=state')
|
||||
.get('/search/suggestions?type=state&includeNull=true')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toHaveLength(states.length);
|
||||
expect(body).toEqual(expect.arrayContaining(states));
|
||||
expect(body).toEqual([
|
||||
'Andalusia',
|
||||
'Berlin',
|
||||
'Glarus',
|
||||
'Greater Accra',
|
||||
'Havana',
|
||||
'Île-de-France',
|
||||
'Marrakesh-Safi',
|
||||
'Mississippi',
|
||||
'New York',
|
||||
'Shanghai',
|
||||
'St.-Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
'Virginia',
|
||||
null,
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should get suggestions for city', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=city')
|
||||
.get('/search/suggestions?type=city&includeNull=true')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual(cities);
|
||||
expect(body).toEqual([
|
||||
'Accra',
|
||||
'Berlin',
|
||||
'Glarus',
|
||||
'Havana',
|
||||
'Marrakesh',
|
||||
'Montalbán de Córdoba',
|
||||
'New York City',
|
||||
'Novena',
|
||||
'Paris',
|
||||
'Philadelphia',
|
||||
'Saint Petersburg',
|
||||
'Shanghai',
|
||||
'Stanley',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
null,
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should get suggestions for camera make', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=camera-make')
|
||||
.get('/search/suggestions?type=camera-make&includeNull=true')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Apple',
|
||||
@@ -485,13 +619,14 @@ describe('/search', () => {
|
||||
'PENTAX Corporation',
|
||||
'samsung',
|
||||
'SONY',
|
||||
null,
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should get suggestions for camera model', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=camera-model')
|
||||
.get('/search/suggestions?type=camera-model&includeNull=true')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Canon EOS 7D',
|
||||
@@ -506,6 +641,7 @@ describe('/search', () => {
|
||||
'SM-F711N',
|
||||
'SM-S906U',
|
||||
'SM-T970',
|
||||
null,
|
||||
]);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -424,12 +424,12 @@ export const utils = {
|
||||
|
||||
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'immich_access_token',
|
||||
value: accessToken,
|
||||
domain: '127.0.0.1',
|
||||
domain,
|
||||
path: '/',
|
||||
expires: 1_742_402_728,
|
||||
httpOnly: true,
|
||||
@@ -439,7 +439,7 @@ export const utils = {
|
||||
{
|
||||
name: 'immich_auth_type',
|
||||
value: 'password',
|
||||
domain: '127.0.0.1',
|
||||
domain,
|
||||
path: '/',
|
||||
expires: 1_742_402_728,
|
||||
httpOnly: true,
|
||||
@@ -449,7 +449,7 @@ export const utils = {
|
||||
{
|
||||
name: 'immich_is_authenticated',
|
||||
value: 'true',
|
||||
domain: '127.0.0.1',
|
||||
domain,
|
||||
path: '/',
|
||||
expires: 1_742_402_728,
|
||||
httpOnly: false,
|
||||
|
||||
@@ -10,6 +10,9 @@ test.describe('Asset Viewer Navbar', () => {
|
||||
utils.initSdk();
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
asset = await utils.createAsset(admin.accessToken);
|
||||
});
|
||||
|
||||
@@ -49,4 +52,14 @@ test.describe('Asset Viewer Navbar', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('actions', () => {
|
||||
test('favorite asset with shortcut', async ({ context, page }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
await page.keyboard.press('f');
|
||||
await expect(page.locator('#notification-list').getByTestId('message')).toHaveText('Added to favorites');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
56
e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts
Normal file
56
e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||
import { expect, type Page, test } from '@playwright/test';
|
||||
import { utils } from 'src/utils';
|
||||
|
||||
test.describe('Slideshow', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset: AssetMediaResponseDto;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
utils.initSdk();
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
asset = await utils.createAsset(admin.accessToken);
|
||||
});
|
||||
|
||||
const openSlideshow = async (page: Page) => {
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
await page.getByRole('button', { name: 'More' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Slideshow' }).click();
|
||||
};
|
||||
|
||||
test('open slideshow', async ({ context, page }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await openSlideshow(page);
|
||||
await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('exit slideshow with button', async ({ context, page }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await openSlideshow(page);
|
||||
|
||||
const exitButton = page.getByRole('button', { name: 'Exit Slideshow' });
|
||||
await exitButton.click();
|
||||
await expect(exitButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('exit slideshow with shortcut', async ({ context, page }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await openSlideshow(page);
|
||||
|
||||
const exitButton = page.getByRole('button', { name: 'Exit Slideshow' });
|
||||
await expect(exitButton).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(exitButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('favorite shortcut is disabled', async ({ context, page }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await openSlideshow(page);
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible();
|
||||
await page.keyboard.press('f');
|
||||
await expect(page.locator('#notification-list')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ test.describe('Photo Viewer', () => {
|
||||
|
||||
test('initially shows a loading spinner', async ({ page }) => {
|
||||
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
|
||||
// slow down the request for thumbnail, so spiner has chance to show up
|
||||
// slow down the request for thumbnail, so spinner has chance to show up
|
||||
await new Promise((f) => setTimeout(f, 2000));
|
||||
await route.continue();
|
||||
});
|
||||
@@ -40,7 +40,7 @@ test.describe('Photo Viewer', () => {
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
||||
const box = await imageLocator(page).boundingBox();
|
||||
expect(box).toBeTruthy;
|
||||
expect(box).toBeTruthy();
|
||||
const { x, y, width, height } = box!;
|
||||
await page.mouse.move(x + width / 2, y + height / 2);
|
||||
await page.mouse.wheel(0, -1);
|
||||
|
||||
25
e2e/src/web/specs/websocket.e2e-spec.ts
Normal file
25
e2e/src/web/specs/websocket.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { utils } from 'src/utils';
|
||||
|
||||
test.describe('Websocket', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
utils.initSdk();
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
});
|
||||
|
||||
test('connects using ipv4', async ({ page, context }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await page.goto('http://127.0.0.1:2283/');
|
||||
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
||||
});
|
||||
|
||||
test('connects using ipv6', async ({ page, context }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken, '[::1]');
|
||||
await page.goto('http://[::1]:2283/');
|
||||
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
|
||||
globalSetup,
|
||||
testTimeout: 15_000,
|
||||
pool: 'threads',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:ef4b550f029a76b94f8e6cc6e4a8ed0e870fc6c5af1c4e9d77faaea50f41f6cd as builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:d0131ce0ff4bdb5e9eae6bc86ebde891c207d5cac1f3f582b5de0f903cc68384 AS builder-cpu
|
||||
|
||||
FROM builder-cpu as builder-openvino
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
FROM builder-cpu as builder-cuda
|
||||
FROM builder-cpu AS builder-cuda
|
||||
|
||||
FROM builder-cpu as builder-armnn
|
||||
FROM builder-cpu AS builder-armnn
|
||||
|
||||
ENV ARMNN_PATH=/opt/armnn
|
||||
COPY ann /opt/ann
|
||||
@@ -15,7 +15,7 @@ RUN mkdir /opt/armnn && \
|
||||
cd /opt/ann && \
|
||||
sh build.sh
|
||||
|
||||
FROM builder-${DEVICE} as builder
|
||||
FROM builder-${DEVICE} AS builder
|
||||
|
||||
ARG DEVICE
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
@@ -34,9 +34,9 @@ RUN python3 -m venv /opt/venv
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:ee317183d292ee6ed30e90bc325043ca3f7d2e8c79ac5019575490b5256ae244 as prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:a90e299af8a9cd6b59c4aaed2b024c78561476978244a1ab89742a4a5ac8c974 AS prod-cpu
|
||||
|
||||
FROM prod-cpu as prod-openvino
|
||||
FROM prod-cpu AS prod-openvino
|
||||
|
||||
COPY scripts/configure-apt.sh ./
|
||||
RUN ./configure-apt.sh && \
|
||||
@@ -44,13 +44,13 @@ RUN ./configure-apt.sh && \
|
||||
apt-get install -t unstable --no-install-recommends -yqq intel-opencl-icd && \
|
||||
rm configure-apt.sh
|
||||
|
||||
FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 as prod-cuda
|
||||
FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 AS prod-cuda
|
||||
|
||||
COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
||||
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
|
||||
|
||||
FROM prod-cpu as prod-armnn
|
||||
FROM prod-cpu AS prod-armnn
|
||||
|
||||
ENV LD_LIBRARY_PATH=/opt/armnn
|
||||
|
||||
@@ -70,7 +70,7 @@ COPY --from=builder-armnn \
|
||||
/opt/ann/build.sh \
|
||||
/opt/armnn/
|
||||
|
||||
FROM prod-${DEVICE} as prod
|
||||
FROM prod-${DEVICE} AS prod
|
||||
ARG DEVICE
|
||||
|
||||
RUN apt-get update && \
|
||||
|
||||
@@ -65,7 +65,7 @@ class Ann(metaclass=_Singleton):
|
||||
self.input_shapes: dict[int, tuple[tuple[int], ...]] = {}
|
||||
self.ann: int | None = None
|
||||
self.new()
|
||||
|
||||
|
||||
if self.tuning_file is not None:
|
||||
# make sure tuning file exists (without clearing contents)
|
||||
# once filled, the tuning file reduces the cost/time of the first
|
||||
@@ -105,7 +105,7 @@ class Ann(metaclass=_Singleton):
|
||||
raise ValueError("model_path must be a file with extension .armnn, .tflite or .onnx")
|
||||
if not exists(model_path):
|
||||
raise ValueError("model_path must point to an existing file!")
|
||||
|
||||
|
||||
save_cached_network = False
|
||||
if cached_network_path is not None and not exists(cached_network_path):
|
||||
save_cached_network = True
|
||||
|
||||
@@ -2,53 +2,64 @@ from app.config import clean_name
|
||||
from app.schemas import ModelSource
|
||||
|
||||
_OPENCLIP_MODELS = {
|
||||
"RN50__openai",
|
||||
"RN50__yfcc15m",
|
||||
"RN50__cc12m",
|
||||
"RN101__openai",
|
||||
"RN101__yfcc15m",
|
||||
"RN50x4__openai",
|
||||
"RN50__cc12m",
|
||||
"RN50__openai",
|
||||
"RN50__yfcc15m",
|
||||
"RN50x16__openai",
|
||||
"RN50x4__openai",
|
||||
"RN50x64__openai",
|
||||
"ViT-B-32__openai",
|
||||
"ViT-B-16-SigLIP-256__webli",
|
||||
"ViT-B-16-SigLIP-384__webli",
|
||||
"ViT-B-16-SigLIP-512__webli",
|
||||
"ViT-B-16-SigLIP-i18n-256__webli",
|
||||
"ViT-B-16-SigLIP__webli",
|
||||
"ViT-B-16-plus-240__laion400m_e31",
|
||||
"ViT-B-16-plus-240__laion400m_e32",
|
||||
"ViT-B-16__laion400m_e31",
|
||||
"ViT-B-16__laion400m_e32",
|
||||
"ViT-B-16__openai",
|
||||
"ViT-B-32__laion2b-s34b-b79k",
|
||||
"ViT-B-32__laion2b_e16",
|
||||
"ViT-B-32__laion400m_e31",
|
||||
"ViT-B-32__laion400m_e32",
|
||||
"ViT-B-32__laion2b-s34b-b79k",
|
||||
"ViT-B-16__openai",
|
||||
"ViT-B-16__laion400m_e31",
|
||||
"ViT-B-16__laion400m_e32",
|
||||
"ViT-B-16-plus-240__laion400m_e31",
|
||||
"ViT-B-16-plus-240__laion400m_e32",
|
||||
"ViT-L-14__openai",
|
||||
"ViT-B-32__openai",
|
||||
"ViT-H-14-378-quickgelu__dfn5b",
|
||||
"ViT-H-14-quickgelu__dfn5b",
|
||||
"ViT-H-14__laion2b-s32b-b79k",
|
||||
"ViT-L-14-336__openai",
|
||||
"ViT-L-14-quickgelu__dfn2b",
|
||||
"ViT-L-14__laion2b-s32b-b82k",
|
||||
"ViT-L-14__laion400m_e31",
|
||||
"ViT-L-14__laion400m_e32",
|
||||
"ViT-L-14__laion2b-s32b-b82k",
|
||||
"ViT-L-14-336__openai",
|
||||
"ViT-H-14__laion2b-s32b-b79k",
|
||||
"ViT-L-14__openai",
|
||||
"ViT-L-16-SigLIP-256__webli",
|
||||
"ViT-L-16-SigLIP-384__webli",
|
||||
"ViT-SO400M-14-SigLIP-384__webli",
|
||||
"ViT-g-14__laion2b-s12b-b42k",
|
||||
"ViT-L-14-quickgelu__dfn2b",
|
||||
"ViT-H-14-quickgelu__dfn5b",
|
||||
"ViT-H-14-378-quickgelu__dfn5b",
|
||||
"XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k",
|
||||
"XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k",
|
||||
"nllb-clip-base-siglip__mrl",
|
||||
"nllb-clip-base-siglip__v1",
|
||||
"nllb-clip-large-siglip__mrl",
|
||||
"nllb-clip-large-siglip__v1",
|
||||
}
|
||||
|
||||
|
||||
_MCLIP_MODELS = {
|
||||
"LABSE-Vit-L-14",
|
||||
"XLM-Roberta-Large-Vit-B-32",
|
||||
"XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"XLM-Roberta-Large-Vit-B-32",
|
||||
"XLM-Roberta-Large-Vit-L-14",
|
||||
}
|
||||
|
||||
|
||||
_INSIGHTFACE_MODELS = {
|
||||
"antelopev2",
|
||||
"buffalo_l",
|
||||
"buffalo_m",
|
||||
"buffalo_s",
|
||||
"buffalo_m",
|
||||
"buffalo_l",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:94d6837f023c0fc0bb68782dd2a984ff7fe0e21ea7e533056c9b8ca060e31de2 as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:954e438daab0ad0835430ea84acb27dd47d1ea35a7120c3c9dd9d1a5578f4b13 AS builder
|
||||
|
||||
ENV TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ name: base
|
||||
channels:
|
||||
- conda-forge
|
||||
- nvidia
|
||||
- pytorch-nightly
|
||||
- pytorch
|
||||
platforms:
|
||||
- linux-64
|
||||
dependencies:
|
||||
@@ -13,7 +13,7 @@ dependencies:
|
||||
- orjson==3.*
|
||||
- pip
|
||||
- python==3.11.*
|
||||
- pytorch
|
||||
- pytorch>=2.3
|
||||
- rich==13.*
|
||||
- safetensors==0.*
|
||||
- setuptools==68.*
|
||||
@@ -21,5 +21,5 @@ dependencies:
|
||||
- transformers==4.*
|
||||
- pip:
|
||||
- multilingual-clip
|
||||
- onnx-simplifier
|
||||
- onnxsim
|
||||
category: main
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
@@ -8,7 +9,6 @@ from transformers import AutoTokenizer
|
||||
|
||||
from .openclip import OpenCLIPModelConfig
|
||||
from .openclip import to_onnx as openclip_to_onnx
|
||||
from .optimize import optimize
|
||||
from .util import get_model_path
|
||||
|
||||
_MCLIP_TO_OPENCLIP = {
|
||||
@@ -23,18 +23,20 @@ def to_onnx(
|
||||
model_name: str,
|
||||
output_dir_visual: Path | str,
|
||||
output_dir_textual: Path | str,
|
||||
) -> None:
|
||||
) -> tuple[Path, Path]:
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model = MultilingualCLIP.from_pretrained(model_name, cache_dir=tmpdir)
|
||||
model = MultilingualCLIP.from_pretrained(model_name, cache_dir=os.environ.get("CACHE_DIR", tmpdir))
|
||||
AutoTokenizer.from_pretrained(model_name).save_pretrained(output_dir_textual)
|
||||
|
||||
model.eval()
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
export_text_encoder(model, textual_path)
|
||||
openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
|
||||
optimize(textual_path)
|
||||
visual_path, _ = openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
|
||||
assert visual_path is not None, "Visual model export failed"
|
||||
return visual_path, textual_path
|
||||
|
||||
|
||||
def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> None:
|
||||
@@ -58,10 +60,10 @@ def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> Non
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["input_ids", "attention_mask"],
|
||||
output_names=["text_embedding"],
|
||||
output_names=["embedding"],
|
||||
opset_version=17,
|
||||
dynamic_axes={
|
||||
"input_ids": {0: "batch_size", 1: "sequence_length"},
|
||||
"attention_mask": {0: "batch_size", 1: "sequence_length"},
|
||||
},
|
||||
# dynamic_axes={
|
||||
# "input_ids": {0: "batch_size", 1: "sequence_length"},
|
||||
# "attention_mask": {0: "batch_size", 1: "sequence_length"},
|
||||
# },
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
@@ -7,7 +8,6 @@ import open_clip
|
||||
import torch
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from .optimize import optimize
|
||||
from .util import get_model_path, save_config
|
||||
|
||||
|
||||
@@ -23,25 +23,28 @@ class OpenCLIPModelConfig:
|
||||
if open_clip_cfg is None:
|
||||
raise ValueError(f"Unknown model {self.name}")
|
||||
self.image_size = open_clip_cfg["vision_cfg"]["image_size"]
|
||||
self.sequence_length = open_clip_cfg["text_cfg"]["context_length"]
|
||||
self.sequence_length = open_clip_cfg["text_cfg"].get("context_length", 77)
|
||||
|
||||
|
||||
def to_onnx(
|
||||
model_cfg: OpenCLIPModelConfig,
|
||||
output_dir_visual: Path | str | None = None,
|
||||
output_dir_textual: Path | str | None = None,
|
||||
) -> None:
|
||||
) -> tuple[Path | None, Path | None]:
|
||||
visual_path = None
|
||||
textual_path = None
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model = open_clip.create_model(
|
||||
model_cfg.name,
|
||||
pretrained=model_cfg.pretrained,
|
||||
jit=False,
|
||||
cache_dir=tmpdir,
|
||||
cache_dir=os.environ.get("CACHE_DIR", tmpdir),
|
||||
require_pretrained=True,
|
||||
)
|
||||
|
||||
text_vision_cfg = open_clip.get_model_config(model_cfg.name)
|
||||
|
||||
model.eval()
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(False)
|
||||
|
||||
@@ -53,8 +56,6 @@ def to_onnx(
|
||||
save_config(text_vision_cfg, output_dir_visual.parent / "config.json")
|
||||
export_image_encoder(model, model_cfg, visual_path)
|
||||
|
||||
optimize(visual_path)
|
||||
|
||||
if output_dir_textual is not None:
|
||||
output_dir_textual = Path(output_dir_textual)
|
||||
textual_path = get_model_path(output_dir_textual)
|
||||
@@ -62,7 +63,7 @@ def to_onnx(
|
||||
tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32")
|
||||
AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual)
|
||||
export_text_encoder(model, model_cfg, textual_path)
|
||||
optimize(textual_path)
|
||||
return visual_path, textual_path
|
||||
|
||||
|
||||
def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
|
||||
@@ -83,9 +84,9 @@ def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig,
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["image"],
|
||||
output_names=["image_embedding"],
|
||||
output_names=["embedding"],
|
||||
opset_version=17,
|
||||
dynamic_axes={"image": {0: "batch_size"}},
|
||||
# dynamic_axes={"image": {0: "batch_size"}},
|
||||
)
|
||||
|
||||
|
||||
@@ -107,7 +108,7 @@ def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, o
|
||||
args,
|
||||
output_path.as_posix(),
|
||||
input_names=["text"],
|
||||
output_names=["text_embedding"],
|
||||
output_names=["embedding"],
|
||||
opset_version=17,
|
||||
dynamic_axes={"text": {0: "batch_size"}},
|
||||
# dynamic_axes={"text": {0: "batch_size"}},
|
||||
)
|
||||
|
||||
@@ -5,13 +5,26 @@ import onnxruntime as ort
|
||||
import onnxsim
|
||||
|
||||
|
||||
def save_onnx(model: onnx.ModelProto, output_path: Path | str) -> None:
|
||||
try:
|
||||
onnx.save(model, output_path)
|
||||
except ValueError as e:
|
||||
if "The proto size is larger than the 2 GB limit." in str(e):
|
||||
onnx.save(model, output_path, save_as_external_data=True, size_threshold=1_000_000)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
def optimize_onnxsim(model_path: Path | str, output_path: Path | str) -> None:
|
||||
model_path = Path(model_path)
|
||||
output_path = Path(output_path)
|
||||
model = onnx.load(model_path.as_posix())
|
||||
model, check = onnxsim.simplify(model, skip_shape_inference=True)
|
||||
model, check = onnxsim.simplify(model)
|
||||
assert check, "Simplified ONNX model could not be validated"
|
||||
onnx.save(model, output_path.as_posix())
|
||||
for file in model_path.parent.iterdir():
|
||||
if file.name.startswith("Constant") or "onnx" in file.name or file.suffix == ".weight":
|
||||
file.unlink()
|
||||
save_onnx(model, output_path)
|
||||
|
||||
|
||||
def optimize_ort(
|
||||
@@ -33,6 +46,4 @@ def optimize(model_path: Path | str) -> None:
|
||||
model_path = Path(model_path)
|
||||
|
||||
optimize_ort(model_path, model_path)
|
||||
# onnxsim serializes large models as a blob, which uses much more memory when loading the model at runtime
|
||||
if not any(file.name.startswith("Constant") for file in model_path.parent.iterdir()):
|
||||
optimize_onnxsim(model_path, model_path)
|
||||
optimize_onnxsim(model_path, model_path)
|
||||
|
||||
@@ -3,74 +3,111 @@ import os
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from huggingface_hub import create_repo, login, upload_folder
|
||||
import torch
|
||||
from huggingface_hub import create_repo, upload_folder
|
||||
from models import mclip, openclip
|
||||
from models.optimize import optimize
|
||||
from rich.progress import Progress
|
||||
|
||||
models = [
|
||||
"RN50::openai",
|
||||
"RN50::yfcc15m",
|
||||
"RN50::cc12m",
|
||||
"M-CLIP/LABSE-Vit-L-14",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
|
||||
"RN101::openai",
|
||||
"RN101::yfcc15m",
|
||||
"RN50x4::openai",
|
||||
"RN50::cc12m",
|
||||
"RN50::openai",
|
||||
"RN50::yfcc15m",
|
||||
"RN50x16::openai",
|
||||
"RN50x4::openai",
|
||||
"RN50x64::openai",
|
||||
"ViT-B-32::openai",
|
||||
"ViT-B-16-SigLIP-256::webli",
|
||||
"ViT-B-16-SigLIP-384::webli",
|
||||
"ViT-B-16-SigLIP-512::webli",
|
||||
"ViT-B-16-SigLIP-i18n-256::webli",
|
||||
"ViT-B-16-SigLIP::webli",
|
||||
"ViT-B-16-plus-240::laion400m_e31",
|
||||
"ViT-B-16-plus-240::laion400m_e32",
|
||||
"ViT-B-16::laion400m_e31",
|
||||
"ViT-B-16::laion400m_e32",
|
||||
"ViT-B-16::openai",
|
||||
"ViT-B-32::laion2b-s34b-b79k",
|
||||
"ViT-B-32::laion2b_e16",
|
||||
"ViT-B-32::laion400m_e31",
|
||||
"ViT-B-32::laion400m_e32",
|
||||
"ViT-B-32::laion2b-s34b-b79k",
|
||||
"ViT-B-16::openai",
|
||||
"ViT-B-16::laion400m_e31",
|
||||
"ViT-B-16::laion400m_e32",
|
||||
"ViT-B-16-plus-240::laion400m_e31",
|
||||
"ViT-B-16-plus-240::laion400m_e32",
|
||||
"ViT-L-14::openai",
|
||||
"ViT-B-32::openai",
|
||||
"ViT-H-14-378-quickgelu::dfn5b",
|
||||
"ViT-H-14-quickgelu::dfn5b",
|
||||
"ViT-H-14::laion2b-s32b-b79k",
|
||||
"ViT-L-14-336::openai",
|
||||
"ViT-L-14-quickgelu::dfn2b",
|
||||
"ViT-L-14::laion2b-s32b-b82k",
|
||||
"ViT-L-14::laion400m_e31",
|
||||
"ViT-L-14::laion400m_e32",
|
||||
"ViT-L-14::laion2b-s32b-b82k",
|
||||
"ViT-L-14-336::openai",
|
||||
"ViT-H-14::laion2b-s32b-b79k",
|
||||
"ViT-L-14::openai",
|
||||
"ViT-L-16-SigLIP-256::webli",
|
||||
"ViT-L-16-SigLIP-384::webli",
|
||||
"ViT-SO400M-14-SigLIP-384::webli",
|
||||
"ViT-g-14::laion2b-s12b-b42k",
|
||||
"M-CLIP/LABSE-Vit-L-14",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
|
||||
"nllb-clip-base-siglip::mrl",
|
||||
"nllb-clip-base-siglip::v1",
|
||||
"nllb-clip-large-siglip::mrl",
|
||||
"nllb-clip-large-siglip::v1",
|
||||
"xlm-roberta-base-ViT-B-32::laion5b_s13b_b90k",
|
||||
"xlm-roberta-large-ViT-H-14::frozen_laion5b_s13b_b90k",
|
||||
]
|
||||
|
||||
login(token=os.environ["HF_AUTH_TOKEN"])
|
||||
# glob to delete old UUID blobs when reuploading models
|
||||
uuid_char = "[a-fA-F0-9]"
|
||||
uuid_glob = uuid_char * 8 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 12
|
||||
|
||||
# remote repo files to be deleted before uploading
|
||||
# deletion is in the same commit as the upload, so it's atomic
|
||||
delete_patterns = ["**/*onnx*", "**/Constant*", "**/*.weight", "**/*.bias", f"**/{uuid_glob}"]
|
||||
|
||||
with Progress() as progress:
|
||||
task1 = progress.add_task("[green]Exporting models...", total=len(models))
|
||||
task2 = progress.add_task("[yellow]Uploading models...", total=len(models))
|
||||
|
||||
task = progress.add_task("[green]Exporting models...", total=len(models))
|
||||
token = os.environ.get("HF_AUTH_TOKEN")
|
||||
torch.backends.mha.set_fastpath_enabled(False)
|
||||
with TemporaryDirectory() as tmp:
|
||||
tmpdir = Path(tmp)
|
||||
for model in models:
|
||||
model_name = model.split("/")[-1].replace("::", "__")
|
||||
hf_model_name = model_name.replace("xlm-roberta-large", "XLM-Roberta-Large")
|
||||
hf_model_name = model_name.replace("xlm-roberta-base", "XLM-Roberta-Base")
|
||||
config_path = tmpdir / model_name / "config.json"
|
||||
|
||||
def upload() -> None:
|
||||
progress.update(task2, description=f"[yellow]Uploading {model_name}")
|
||||
repo_id = f"immich-app/{model_name}"
|
||||
|
||||
create_repo(repo_id, exist_ok=True)
|
||||
upload_folder(repo_id=repo_id, folder_path=tmpdir / model_name)
|
||||
progress.update(task2, advance=1)
|
||||
|
||||
def export() -> None:
|
||||
progress.update(task1, description=f"[green]Exporting {model_name}")
|
||||
visual_dir = tmpdir / model_name / "visual"
|
||||
textual_dir = tmpdir / model_name / "textual"
|
||||
progress.update(task, description=f"[green]Exporting {hf_model_name}")
|
||||
visual_dir = tmpdir / hf_model_name / "visual"
|
||||
textual_dir = tmpdir / hf_model_name / "textual"
|
||||
if model.startswith("M-CLIP"):
|
||||
mclip.to_onnx(model, visual_dir, textual_dir)
|
||||
visual_path, textual_path = mclip.to_onnx(model, visual_dir, textual_dir)
|
||||
else:
|
||||
name, _, pretrained = model_name.partition("__")
|
||||
openclip.to_onnx(openclip.OpenCLIPModelConfig(name, pretrained), visual_dir, textual_dir)
|
||||
config = openclip.OpenCLIPModelConfig(name, pretrained)
|
||||
visual_path, textual_path = openclip.to_onnx(config, visual_dir, textual_dir)
|
||||
progress.update(task, description=f"[green]Optimizing {hf_model_name} (visual)")
|
||||
optimize(visual_path)
|
||||
progress.update(task, description=f"[green]Optimizing {hf_model_name} (textual)")
|
||||
optimize(textual_path)
|
||||
|
||||
progress.update(task1, advance=1)
|
||||
gc.collect()
|
||||
|
||||
def upload() -> None:
|
||||
progress.update(task, description=f"[yellow]Uploading {hf_model_name}")
|
||||
repo_id = f"immich-app/{hf_model_name}"
|
||||
|
||||
create_repo(repo_id, exist_ok=True)
|
||||
upload_folder(
|
||||
repo_id=repo_id,
|
||||
folder_path=tmpdir / hf_model_name,
|
||||
delete_patterns=delete_patterns,
|
||||
token=token,
|
||||
)
|
||||
|
||||
export()
|
||||
upload()
|
||||
if token is not None:
|
||||
upload()
|
||||
progress.update(task, advance=1)
|
||||
|
||||
143
machine-learning/poetry.lock
generated
143
machine-learning/poetry.lock
generated
@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.24.0"
|
||||
version = "0.24.5"
|
||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "huggingface_hub-0.24.0-py3-none-any.whl", hash = "sha256:7ad92edefb93d8145c061f6df8d99df2ff85f8379ba5fac8a95aca0642afa5d7"},
|
||||
{file = "huggingface_hub-0.24.0.tar.gz", hash = "sha256:6c7092736b577d89d57b3cdfea026f1b0dc2234ae783fa0d59caf1bf7d52dfa7"},
|
||||
{file = "huggingface_hub-0.24.5-py3-none-any.whl", hash = "sha256:d93fb63b1f1a919a22ce91a14518974e81fc4610bf344dfe7572343ce8d3aced"},
|
||||
{file = "huggingface_hub-0.24.5.tar.gz", hash = "sha256:7b45d6744dd53ce9cbf9880957de00e9d10a9ae837f1c9b7255fc8fa4e8264f3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.29.1"
|
||||
version = "2.31.1"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.29.1-py3-none-any.whl", hash = "sha256:8b15daab44cdf50eef1860a32bb30969423e3795247115e5a37446da3240c6d6"},
|
||||
{file = "locust-2.29.1.tar.gz", hash = "sha256:2e0628a59e2689a50cb4735a9a43709e30f2da7ed276c15d877c5325507f44b1"},
|
||||
{file = "locust-2.31.1-py3-none-any.whl", hash = "sha256:20756509939004e95c622ac3042886edab38b736f00534cc03ce2774064e7f71"},
|
||||
{file = "locust-2.31.1.tar.gz", hash = "sha256:d26b7333cdef80645f3978d8ff9aabab4d53e41ed82cc8490212aa68e8498fdd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1548,14 +1548,14 @@ gevent = ">=22.10.2"
|
||||
geventhttpclient = ">=2.3.1"
|
||||
msgpack = ">=1.0.0"
|
||||
psutil = ">=5.9.1"
|
||||
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
pyzmq = ">=25.0.0"
|
||||
requests = [
|
||||
{version = ">=2.32.2", markers = "python_version > \"3.11\""},
|
||||
{version = ">=2.26.0", markers = "python_version <= \"3.11\""},
|
||||
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
|
||||
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
|
||||
]
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""}
|
||||
typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""}
|
||||
Werkzeug = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
@@ -1794,38 +1794,38 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"},
|
||||
{file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"},
|
||||
{file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"},
|
||||
{file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"},
|
||||
{file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"},
|
||||
{file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"},
|
||||
{file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"},
|
||||
{file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"},
|
||||
{file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"},
|
||||
{file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"},
|
||||
{file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"},
|
||||
{file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"},
|
||||
{file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"},
|
||||
{file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"},
|
||||
{file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"},
|
||||
{file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"},
|
||||
{file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"},
|
||||
{file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"},
|
||||
{file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"},
|
||||
{file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"},
|
||||
{file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"},
|
||||
{file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"},
|
||||
{file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"},
|
||||
{file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"},
|
||||
{file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"},
|
||||
{file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"},
|
||||
{file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
|
||||
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
|
||||
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2074,10 +2074,10 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
|
||||
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2466,13 +2466,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.2.2"
|
||||
version = "8.3.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
|
||||
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
|
||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2480,7 +2480,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2.0"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
@@ -2827,29 +2827,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.4"
|
||||
version = "0.5.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"},
|
||||
{file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"},
|
||||
{file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"},
|
||||
{file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"},
|
||||
{file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"},
|
||||
{file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"},
|
||||
{file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"},
|
||||
{file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"},
|
||||
{file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"},
|
||||
{file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"},
|
||||
{file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"},
|
||||
{file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"},
|
||||
{file = "ruff-0.5.6-py3-none-linux_armv6l.whl", hash = "sha256:a0ef5930799a05522985b9cec8290b185952f3fcd86c1772c3bdbd732667fdcd"},
|
||||
{file = "ruff-0.5.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b652dc14f6ef5d1552821e006f747802cc32d98d5509349e168f6bf0ee9f8f42"},
|
||||
{file = "ruff-0.5.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80521b88d26a45e871f31e4b88938fd87db7011bb961d8afd2664982dfc3641a"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9bc8f328a9f1309ae80e4d392836e7dbc77303b38ed4a7112699e63d3b066ab"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d394940f61f7720ad371ddedf14722ee1d6250fd8d020f5ea5a86e7be217daf"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111a99cdb02f69ddb2571e2756e017a1496c2c3a2aeefe7b988ddab38b416d36"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e395daba77a79f6dc0d07311f94cc0560375ca20c06f354c7c99af3bf4560c5d"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c476acb43c3c51e3c614a2e878ee1589655fa02dab19fe2db0423a06d6a5b1b6"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2ff8003f5252fd68425fd53d27c1f08b201d7ed714bb31a55c9ac1d4c13e2eb"},
|
||||
{file = "ruff-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c94e084ba3eaa80c2172918c2ca2eb2230c3f15925f4ed8b6297260c6ef179ad"},
|
||||
{file = "ruff-0.5.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f77c1c3aa0669fb230b06fb24ffa3e879391a3ba3f15e3d633a752da5a3e670"},
|
||||
{file = "ruff-0.5.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f908148c93c02873210a52cad75a6eda856b2cbb72250370ce3afef6fb99b1ed"},
|
||||
{file = "ruff-0.5.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:563a7ae61ad284187d3071d9041c08019975693ff655438d8d4be26e492760bd"},
|
||||
{file = "ruff-0.5.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:94fe60869bfbf0521e04fd62b74cbca21cbc5beb67cbb75ab33fe8c174f54414"},
|
||||
{file = "ruff-0.5.6-py3-none-win32.whl", hash = "sha256:e6a584c1de6f8591c2570e171cc7ce482bb983d49c70ddf014393cd39e9dfaed"},
|
||||
{file = "ruff-0.5.6-py3-none-win_amd64.whl", hash = "sha256:d7fe7dccb1a89dc66785d7aa0ac283b2269712d8ed19c63af908fdccca5ccc1a"},
|
||||
{file = "ruff-0.5.6-py3-none-win_arm64.whl", hash = "sha256:57c6c0dd997b31b536bff49b9eee5ed3194d60605a4427f735eeb1f9c1b8d264"},
|
||||
{file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2991,19 +2991,18 @@ test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeo
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "68.2.2"
|
||||
version = "70.3.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
|
||||
{file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
|
||||
{file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"},
|
||||
{file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@@ -3263,13 +3262,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.30.1"
|
||||
version = "0.30.5"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
|
||||
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
|
||||
{file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"},
|
||||
{file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3601,4 +3600,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "df9afeda50e05cb62b322a047028a9b0851db197c4f379903c70adab3a98777a"
|
||||
content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab"
|
||||
|
||||
@@ -17,7 +17,7 @@ pydantic = "^1.10.8"
|
||||
aiocache = ">=0.12.1,<1.0"
|
||||
rich = ">=13.4.2"
|
||||
ftfy = ">=6.1.1"
|
||||
setuptools = "^68.0.0"
|
||||
setuptools = "^70.0.0"
|
||||
python-multipart = ">=0.0.6,<1.0"
|
||||
orjson = ">=3.9.5"
|
||||
gunicorn = ">=21.1.0"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
|
||||
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" android:largeHeap="true">
|
||||
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
|
||||
android:largeHeap="true">
|
||||
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
@@ -55,7 +56,8 @@
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
|
||||
@@ -65,6 +67,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -76,4 +79,4 @@
|
||||
<data android:scheme="geo" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -8,9 +8,11 @@ allprojects {
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ pluginManagement {
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.4.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.24" apply false
|
||||
id "org.jetbrains.kotlin.kapt" version "1.9.24" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
|
||||
id "org.jetbrains.kotlin.kapt" version "1.9.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -531,6 +531,11 @@
|
||||
"theme_setting_dark_mode_switch": "Dark mode",
|
||||
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
|
||||
"theme_setting_image_viewer_quality_title": "Image viewer quality",
|
||||
"theme_setting_primary_color_title": "Primary color",
|
||||
"theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
|
||||
"theme_setting_colorful_interface_title": "Colorful interface",
|
||||
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
||||
"theme_setting_system_primary_color_title": "Use system color",
|
||||
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
|
||||
"theme_setting_theme_subtitle": "Choose the app's theme setting",
|
||||
"theme_setting_theme_title": "Theme",
|
||||
@@ -562,4 +567,4 @@
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,6 @@ PODS:
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- Toast
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
@@ -73,7 +70,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.1.1):
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- photo_manager (2.0.0):
|
||||
- Flutter
|
||||
@@ -90,7 +87,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
@@ -123,7 +120,7 @@ DEPENDENCIES:
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
@@ -132,7 +129,6 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- FMDB
|
||||
- MapLibre
|
||||
- ReachabilitySwift
|
||||
- SAMKeychain
|
||||
@@ -184,7 +180,7 @@ EXTERNAL SOURCES:
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
@@ -200,33 +196,32 @@ SPEC CHECKSUMS:
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
||||
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
|
||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
|
||||
C494C1A226E78FAB736DAB6C /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -267,6 +268,23 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||
};
|
||||
C494C1A226E78FAB736DAB6C /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -383,7 +401,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 165;
|
||||
CURRENT_PROJECT_VERSION = 167;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -525,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 165;
|
||||
CURRENT_PROJECT_VERSION = 167;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -553,7 +571,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 165;
|
||||
CURRENT_PROJECT_VERSION = 167;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.110.0</string>
|
||||
<string>1.111.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>165</string>
|
||||
<string>167</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -1,5 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
|
||||
const Color immichBackgroundColor = Color(0xFFf6f8fe);
|
||||
const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0);
|
||||
const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250);
|
||||
enum ImmichColorPreset {
|
||||
indigo,
|
||||
deepPurple,
|
||||
pink,
|
||||
red,
|
||||
orange,
|
||||
yellow,
|
||||
lime,
|
||||
green,
|
||||
cyan,
|
||||
slateGray
|
||||
}
|
||||
|
||||
const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo;
|
||||
const String defaultColorPresetName = "indigo";
|
||||
|
||||
const Color immichBrandColorLight = Color(0xFF4150AF);
|
||||
const Color immichBrandColorDark = Color(0xFFACCBFA);
|
||||
|
||||
final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
|
||||
ImmichColorPreset.indigo: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(
|
||||
seedColor: immichBrandColorLight,
|
||||
).copyWith(primary: immichBrandColorLight),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: immichBrandColorDark,
|
||||
brightness: Brightness.dark,
|
||||
).copyWith(primary: immichBrandColorDark),
|
||||
),
|
||||
ImmichColorPreset.deepPurple: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFF6F43C0)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFD3BBFF),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.pink: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFFED79B5)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFED79B5),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.red: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFFC51C16)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFD3302F),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.orange: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xffff5b01),
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.fidelity,
|
||||
),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFCC6D08),
|
||||
brightness: Brightness.dark,
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.fidelity,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.yellow: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFFFFB400)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFFFB400),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.lime: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFFCDDC39)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFFCDDC39),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.green: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFF18C249)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF18C249),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.cyan: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: const Color(0xFF00BCD4)),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF00BCD4),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
ImmichColorPreset.slateGray: ImmichTheme(
|
||||
light: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF696969),
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.neutral,
|
||||
),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xff696969),
|
||||
brightness: Brightness.dark,
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.neutral,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
extension ImmichColorModeExtension on ImmichColorPreset {
|
||||
ImmichTheme getTheme() => _themePresetsMap[this]!;
|
||||
}
|
||||
|
||||
@@ -229,6 +229,11 @@ enum StoreKey<T> {
|
||||
mapwithPartners<bool>(125, type: bool),
|
||||
enableHapticFeedback<bool>(126, type: bool),
|
||||
customHeaders<String>(127, type: String),
|
||||
|
||||
// theme settings
|
||||
primaryColor<String>(128, type: String),
|
||||
dynamicTheme<bool>(129, type: bool),
|
||||
colorfulInterface<bool>(130, type: bool),
|
||||
;
|
||||
|
||||
const StoreKey(
|
||||
|
||||
@@ -20,10 +20,10 @@ extension ContextHelper on BuildContext {
|
||||
bool get isDarkTheme => themeData.brightness == Brightness.dark;
|
||||
|
||||
// Returns the current Primary color of the Theme
|
||||
Color get primaryColor => themeData.primaryColor;
|
||||
Color get primaryColor => themeData.colorScheme.primary;
|
||||
|
||||
// Returns the Scaffold background color of the Theme
|
||||
Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor;
|
||||
Color get scaffoldBackgroundColor => colorScheme.surface;
|
||||
|
||||
// Returns the current TextTheme
|
||||
TextTheme get textTheme => themeData.textTheme;
|
||||
|
||||
24
mobile/lib/extensions/theme_extensions.dart
Normal file
24
mobile/lib/extensions/theme_extensions.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ImmichColorSchemeExtensions on ColorScheme {
|
||||
bool get _isDarkMode => brightness == Brightness.dark;
|
||||
Color get onSurfaceSecondary => _isDarkMode
|
||||
? onSurface.darken(amount: .3)
|
||||
: onSurface.lighten(amount: .3);
|
||||
}
|
||||
|
||||
extension ColorExtensions on Color {
|
||||
Color lighten({double amount = 0.1}) {
|
||||
return Color.alphaBlend(
|
||||
Colors.white.withOpacity(amount),
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
Color darken({double amount = 0.1}) {
|
||||
return Color.alphaBlend(
|
||||
Colors.black.withOpacity(amount),
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,8 @@ Future<void> initApp() async {
|
||||
}
|
||||
}
|
||||
|
||||
await fetchSystemPalette();
|
||||
|
||||
// Initialize Immich Logger Service
|
||||
ImmichLogger();
|
||||
|
||||
@@ -187,6 +189,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var router = ref.watch(appRouterProvider);
|
||||
var immichTheme = ref.watch(immichThemeProvider);
|
||||
|
||||
return MaterialApp(
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
@@ -196,9 +199,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
home: MaterialApp.router(
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: ref.watch(immichThemeProvider),
|
||||
darkTheme: immichDarkTheme,
|
||||
theme: immichLightTheme,
|
||||
themeMode: ref.watch(immichThemeModeProvider),
|
||||
darkTheme: getThemeData(colorScheme: immichTheme.dark),
|
||||
theme: getThemeData(colorScheme: immichTheme.light),
|
||||
routeInformationParser: router.defaultRouteParser(),
|
||||
routerDelegate: router.delegate(
|
||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
||||
|
||||
@@ -43,7 +43,7 @@ class Activity {
|
||||
assetId = dto.assetId,
|
||||
comment = dto.comment,
|
||||
createdAt = dto.createdAt,
|
||||
type = dto.type == ActivityResponseDtoTypeEnum.comment
|
||||
type = dto.type == ReactionType.comment
|
||||
? ActivityType.comment
|
||||
: ActivityType.like,
|
||||
user = User.fromSimpleUserDto(dto.user);
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
@@ -46,7 +48,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||
"ID ${album.id}",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/widgets/backup/album_info_card.dart';
|
||||
@@ -128,13 +127,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
album.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDarkTheme ? Colors.black : immichBackgroundColor,
|
||||
color: context.scaffoldBackgroundColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.red[300],
|
||||
deleteIconColor:
|
||||
isDarkTheme ? Colors.black : immichBackgroundColor,
|
||||
deleteIconColor: context.scaffoldBackgroundColor,
|
||||
deleteIcon: const Icon(
|
||||
Icons.cancel_rounded,
|
||||
size: 15,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
@@ -17,6 +18,7 @@ import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
@RoutePage()
|
||||
class BackupControllerPage extends HookConsumerWidget {
|
||||
@@ -48,7 +50,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
ref
|
||||
.watch(websocketProvider.notifier)
|
||||
.stopListenToEvent('on_upload_success');
|
||||
return null;
|
||||
|
||||
WakelockPlus.enable();
|
||||
return () {
|
||||
WakelockPlus.disable();
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -130,9 +136,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 56, 56, 56)
|
||||
: Colors.black12,
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -151,7 +155,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
"backup_controller_page_to_backup",
|
||||
style: context.textTheme.bodyMedium,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
).tr(),
|
||||
buildSelectedAlbumName(),
|
||||
buildExcludedAlbumName(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
|
||||
@RoutePage<List<String>?>()
|
||||
@RoutePage()
|
||||
class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
final Album album;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
@RoutePage<AssetSelectionPageResult?>()
|
||||
@RoutePage()
|
||||
class AlbumAssetSelectionPage extends HookConsumerWidget {
|
||||
const AlbumAssetSelectionPage({
|
||||
super.key,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
@@ -102,7 +103,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
backgroundColor: context.colorScheme.surfaceContainer,
|
||||
isScrollControlled: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@@ -131,7 +132,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
),
|
||||
subtitle: Text(
|
||||
album.owner.value?.email ?? "",
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
trailing: Text(
|
||||
"shared_album_section_people_owner_label",
|
||||
@@ -160,7 +161,9 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
),
|
||||
subtitle: Text(
|
||||
user.email,
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
trailing: userId == user.id || isOwner
|
||||
? const Icon(Icons.more_horiz_rounded)
|
||||
@@ -214,7 +217,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
subtitle: Text(
|
||||
"shared_album_activity_setting_subtitle",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.textTheme.labelLarge?.color?.withAlpha(175),
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
|
||||
@RoutePage<List<String>>()
|
||||
@RoutePage()
|
||||
class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
const AlbumSharedUserSelectionPage({super.key, required this.assets});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||
@@ -114,13 +114,13 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
AlbumActionOutlinedButton(
|
||||
AlbumActionFilledButton(
|
||||
iconData: Icons.add_photo_alternate_outlined,
|
||||
onPressed: () => onAddPhotosPressed(album),
|
||||
labelText: "share_add_photos".tr(),
|
||||
),
|
||||
if (userId == album.ownerId)
|
||||
AlbumActionOutlinedButton(
|
||||
AlbumActionFilledButton(
|
||||
iconData: Icons.person_add_alt_rounded,
|
||||
onPressed: () => onAddUsersPressed(album),
|
||||
labelText: "album_viewer_page_share_add_users".tr(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
@@ -18,7 +19,6 @@ class AppLogPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final immichLogger = ImmichLogger();
|
||||
final logMessages = useState(immichLogger.messages);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
Widget colorStatusIndicator(Color color) {
|
||||
return Column(
|
||||
@@ -55,13 +55,9 @@ class AppLogPage extends HookConsumerWidget {
|
||||
case LogLevel.INFO:
|
||||
return Colors.transparent;
|
||||
case LogLevel.SEVERE:
|
||||
return isDarkTheme
|
||||
? Colors.redAccent.withOpacity(0.25)
|
||||
: Colors.redAccent.withOpacity(0.075);
|
||||
return Colors.redAccent.withOpacity(0.25);
|
||||
case LogLevel.WARNING:
|
||||
return isDarkTheme
|
||||
? Colors.orangeAccent.withOpacity(0.25)
|
||||
: Colors.orangeAccent.withOpacity(0.075);
|
||||
return Colors.orangeAccent.withOpacity(0.25);
|
||||
default:
|
||||
return context.primaryColor.withOpacity(0.1);
|
||||
}
|
||||
@@ -120,10 +116,7 @@ class AppLogPage extends HookConsumerWidget {
|
||||
),
|
||||
body: ListView.separated(
|
||||
separatorBuilder: (context, index) {
|
||||
return Divider(
|
||||
height: 0,
|
||||
color: isDarkTheme ? Colors.white70 : Colors.grey[600],
|
||||
);
|
||||
return const Divider(height: 0);
|
||||
},
|
||||
itemCount: logMessages.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -141,8 +134,9 @@ class AppLogPage extends HookConsumerWidget {
|
||||
minLeadingWidth: 10,
|
||||
title: Text(
|
||||
truncateLogMessage(logMessage.message, 4),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: context.colorScheme.onSurface,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
@@ -150,7 +144,7 @@ class AppLogPage extends HookConsumerWidget {
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Colors.grey[600],
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
leading: buildLeadingIcon(logMessage.level),
|
||||
|
||||
@@ -13,8 +13,6 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var isDarkTheme = context.isDarkTheme;
|
||||
|
||||
buildTextWithCopyButton(String header, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -61,7 +59,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Padding(
|
||||
@@ -100,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Padding(
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
||||
|
||||
@@ -109,20 +109,16 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
if (selectedAssets.value.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 63, 63, 63)
|
||||
: const Color.fromARGB(255, 129, 129, 129),
|
||||
),
|
||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
onPressed: onSelectPhotosButtonPressed,
|
||||
icon: Icon(
|
||||
@@ -134,6 +130,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
child: Text(
|
||||
'create_shared_album_page_share_select_photos',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
@@ -150,11 +147,11 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
height: 42,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
AlbumActionOutlinedButton(
|
||||
AlbumActionFilledButton(
|
||||
iconData: Icons.add_photo_alternate_outlined,
|
||||
onPressed: onSelectPhotosButtonPressed,
|
||||
labelText: "share_add_photos".tr(),
|
||||
@@ -266,7 +263,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
pinned: true,
|
||||
floating: false,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(66.0),
|
||||
preferredSize: const Size.fromHeight(96.0),
|
||||
child: Column(
|
||||
children: [
|
||||
buildTitleInputField(),
|
||||
|
||||
@@ -49,10 +49,6 @@ class SettingsPage extends StatelessWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(height: 1),
|
||||
),
|
||||
title: const Text('setting_pages_app_bar_settings').tr(),
|
||||
),
|
||||
body: context.isMobile ? _MobileLayout() : _TabletLayout(),
|
||||
@@ -67,13 +63,18 @@ class _MobileLayout extends StatelessWidget {
|
||||
children: SettingSection.values
|
||||
.map(
|
||||
(s) => ListTile(
|
||||
title: Text(
|
||||
s.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0),
|
||||
leading: Icon(s.icon),
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
s.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
onTap: () => context.pushRoute(SettingsSubRoute(section: s)),
|
||||
),
|
||||
)
|
||||
@@ -102,7 +103,7 @@ class _TabletLayout extends HookWidget {
|
||||
leading: Icon(s.icon),
|
||||
selected: s.index == selectedSection.value.index,
|
||||
selectedColor: context.primaryColor,
|
||||
selectedTileColor: context.primaryColor.withAlpha(50),
|
||||
selectedTileColor: context.themeData.highlightColor,
|
||||
onTap: () => selectedSection.value = s,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -192,6 +192,7 @@ class _AspectRatioButton extends StatelessWidget {
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: () {
|
||||
cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9);
|
||||
aspectRatio.value = ratio;
|
||||
cropController.aspectRatio = ratio;
|
||||
},
|
||||
|
||||
@@ -97,8 +97,10 @@ class EditImagePage extends ConsumerWidget {
|
||||
gravity: ToastGravity.CENTER,
|
||||
);
|
||||
|
||||
await PhotoManager.editor
|
||||
.saveImage(imageData, title: "_edited.jpg");
|
||||
await PhotoManager.editor.saveImage(
|
||||
imageData,
|
||||
title: '${asset!.fileName}_edited.jpg',
|
||||
);
|
||||
await ref.read(albumProvider.notifier).getDeviceAlbums();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
} catch (e) {
|
||||
|
||||
@@ -20,7 +20,6 @@ class LibraryPage extends HookConsumerWidget {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
final albums = ref.watch(albumProvider);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
|
||||
@@ -116,12 +115,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkTheme
|
||||
? const Color.fromARGB(255, 53, 53, 53)
|
||||
: const Color.fromARGB(255, 203, 203, 203),
|
||||
),
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Center(
|
||||
@@ -139,7 +133,9 @@ class LibraryPage extends HookConsumerWidget {
|
||||
),
|
||||
child: Text(
|
||||
'library_page_new_album',
|
||||
style: context.textTheme.labelLarge,
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
@@ -156,26 +152,25 @@ class LibraryPage extends HookConsumerWidget {
|
||||
Function() onClick,
|
||||
) {
|
||||
return Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
child: FilledButton.icon(
|
||||
onPressed: onClick,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: context.isDarkTheme
|
||||
? Colors.white
|
||||
: Colors.black.withAlpha(200),
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||
side: BorderSide(
|
||||
color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!,
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
backgroundColor: context.colorScheme.surfaceContainer,
|
||||
alignment: Alignment.centerLeft,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
icon,
|
||||
@@ -247,6 +242,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
Text(
|
||||
'library_page_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/login_form.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -39,8 +40,8 @@ class LoginPage extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
'v${appVersion.value}',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
|
||||
@RoutePage<LatLng?>()
|
||||
@RoutePage()
|
||||
class MapLocationPickerPage extends HookConsumerWidget {
|
||||
final LatLng initialLatLng;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||
@@ -38,7 +39,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
fontSize: 15.0,
|
||||
);
|
||||
|
||||
Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
Color categoryIconColor = context.colorScheme.onSurface;
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
@@ -128,13 +129,9 @@ class SearchPage extends HookConsumerWidget {
|
||||
},
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? Colors.grey[800]!
|
||||
: const Color.fromARGB(255, 225, 225, 225),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
@@ -144,13 +141,15 @@ class SearchPage extends HookConsumerWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, color: context.primaryColor),
|
||||
Icon(
|
||||
Icons.search,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Text(
|
||||
"search_bar_hint",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color:
|
||||
context.isDarkTheme ? Colors.white70 : Colors.black54,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
).tr(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
@@ -509,7 +510,7 @@ class SearchInputPage extends HookConsumerWidget {
|
||||
? 'contextual_search'.tr()
|
||||
: 'filename_search'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
|
||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
|
||||
@@ -30,6 +30,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const padding = 20.0;
|
||||
final themeData = context.themeData;
|
||||
final colorScheme = context.colorScheme;
|
||||
final descriptionController =
|
||||
useTextEditingController(text: existingLink?.description ?? "");
|
||||
final descriptionFocusNode = useFocusNode();
|
||||
@@ -58,7 +59,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
Text(
|
||||
existingLink!.title,
|
||||
style: TextStyle(
|
||||
color: themeData.primaryColor,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -81,7 +82,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
child: Text(
|
||||
existingLink!.description ?? "--",
|
||||
style: TextStyle(
|
||||
color: themeData.primaryColor,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -109,7 +110,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
labelText: 'shared_link_edit_description'.tr(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeData.primaryColor,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -135,7 +136,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
labelText: 'shared_link_edit_password'.tr(),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeData.primaryColor,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -157,7 +158,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => showMetadata.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
activeColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_show_meta",
|
||||
@@ -173,7 +174,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => allowDownload.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
activeColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_allow_download",
|
||||
@@ -189,7 +190,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => allowUpload.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
activeColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_allow_upload",
|
||||
@@ -205,7 +206,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
onChanged: newShareLink.value.isEmpty
|
||||
? (value) => editExpiry.value = value
|
||||
: null,
|
||||
activeColor: themeData.primaryColor,
|
||||
activeColor: colorScheme.primary,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_link_edit_change_expiry",
|
||||
@@ -221,7 +222,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
"shared_link_edit_expire_after",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeData.primaryColor,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
).tr(),
|
||||
enableSearch: false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
@@ -83,20 +84,24 @@ class SharingPage extends HookConsumerWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.primaryColor,
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: isOwner
|
||||
? Text(
|
||||
'album_thumbnail_owned'.tr(),
|
||||
style: context.textTheme.bodyMedium,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: album.ownerName != null
|
||||
? Text(
|
||||
'album_thumbnail_shared_by'
|
||||
.tr(args: [album.ownerName!]),
|
||||
style: context.textTheme.bodyMedium,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => context
|
||||
@@ -166,11 +171,13 @@ class SharingPage extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
side: BorderSide(
|
||||
color: Colors.grey,
|
||||
width: 0.5,
|
||||
color: context.isDarkTheme
|
||||
? const Color(0xFF383838)
|
||||
: Colors.black12,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
|
||||
@@ -22,9 +22,6 @@ Future<List<PersonResponseDto>> getAllPeople(
|
||||
Future<RenderList> personAssets(PersonAssetsRef ref, String personId) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
final assets = await personService.getPersonAssets(personId);
|
||||
if (assets == null) {
|
||||
return RenderList.empty();
|
||||
}
|
||||
|
||||
final settings = ref.read(appSettingsServiceProvider);
|
||||
final groupBy =
|
||||
|
||||
@@ -21,7 +21,7 @@ final getAllPeopleProvider =
|
||||
);
|
||||
|
||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonResponseDto>>;
|
||||
String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d';
|
||||
String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
@@ -69,7 +68,7 @@ import 'package:photo_manager/photo_manager.dart' hide LatLng;
|
||||
part 'router.gr.dart';
|
||||
|
||||
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
|
||||
class AppRouter extends _$AppRouter {
|
||||
class AppRouter extends RootStackRouter {
|
||||
late final AuthGuard _authGuard;
|
||||
late final DuplicateGuard _duplicateGuard;
|
||||
late final BackupPermissionGuard _backupPermissionGuard;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
|
||||
enum AppSettingsEnum<T> {
|
||||
@@ -8,6 +9,21 @@ enum AppSettingsEnum<T> {
|
||||
"themeMode",
|
||||
"system",
|
||||
), // "light","dark","system"
|
||||
primaryColor<String>(
|
||||
StoreKey.primaryColor,
|
||||
"primaryColor",
|
||||
defaultColorPresetName,
|
||||
),
|
||||
dynamicTheme<bool>(
|
||||
StoreKey.dynamicTheme,
|
||||
"dynamicTheme",
|
||||
false,
|
||||
),
|
||||
colorfulInterface<bool>(
|
||||
StoreKey.colorfulInterface,
|
||||
"colorfulInterface",
|
||||
true,
|
||||
),
|
||||
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
|
||||
dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false),
|
||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
|
||||
|
||||
@@ -30,15 +30,41 @@ class PersonService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> getPersonAssets(String id) async {
|
||||
Future<List<Asset>> getPersonAssets(String id) async {
|
||||
List<Asset> result = [];
|
||||
var hasNext = true;
|
||||
var currentPage = 1;
|
||||
|
||||
try {
|
||||
final assets = await _apiService.peopleApi.getPersonAssets(id);
|
||||
if (assets == null) return null;
|
||||
return await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
|
||||
while (hasNext) {
|
||||
final response = await _apiService.searchApi.searchMetadata(
|
||||
MetadataSearchDto(
|
||||
personIds: [id],
|
||||
page: currentPage,
|
||||
size: 1000,
|
||||
),
|
||||
);
|
||||
|
||||
if (response == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.assets.nextPage == null) {
|
||||
hasNext = false;
|
||||
}
|
||||
|
||||
final assets = response.assets.items;
|
||||
final mapAssets =
|
||||
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
|
||||
result.addAll(mapAssets);
|
||||
|
||||
currentPage++;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error while fetching person assets", error, stack);
|
||||
}
|
||||
return null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<PersonResponseDto?> updateName(String id, String name) async {
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
|
||||
final immichThemeProvider = StateProvider<ThemeMode>((ref) {
|
||||
class ImmichTheme {
|
||||
ColorScheme light;
|
||||
ColorScheme dark;
|
||||
|
||||
ImmichTheme({required this.light, required this.dark});
|
||||
}
|
||||
|
||||
ImmichTheme? _immichDynamicTheme;
|
||||
bool get isDynamicThemeAvailable => _immichDynamicTheme != null;
|
||||
|
||||
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
|
||||
var themeMode = ref
|
||||
.watch(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.themeMode);
|
||||
@@ -20,266 +32,241 @@ final immichThemeProvider = StateProvider<ThemeMode>((ref) {
|
||||
}
|
||||
});
|
||||
|
||||
final ThemeData base = ThemeData(
|
||||
chipTheme: const ChipThemeData(
|
||||
side: BorderSide.none,
|
||||
),
|
||||
sliderTheme: const SliderThemeData(
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
trackHeight: 2.0,
|
||||
),
|
||||
);
|
||||
final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
|
||||
var appSettingsProvider = ref.watch(appSettingsServiceProvider);
|
||||
var primaryColorName =
|
||||
appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
|
||||
|
||||
final ThemeData immichLightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
),
|
||||
primarySwatch: Colors.indigo,
|
||||
primaryColor: Colors.indigo,
|
||||
hintColor: Colors.indigo,
|
||||
focusColor: Colors.indigo,
|
||||
splashColor: Colors.indigo.withOpacity(0.15),
|
||||
fontFamily: 'Overpass',
|
||||
scaffoldBackgroundColor: immichBackgroundColor,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
contentTextStyle: TextStyle(
|
||||
fontFamily: 'Overpass',
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Overpass',
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
backgroundColor: immichBackgroundColor,
|
||||
foregroundColor: Colors.indigo,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: immichBackgroundColor,
|
||||
selectedItemColor: Colors.indigo,
|
||||
),
|
||||
cardTheme: const CardTheme(
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
drawerTheme: const DrawerThemeData(
|
||||
backgroundColor: immichBackgroundColor,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
chipTheme: base.chipTheme,
|
||||
sliderTheme: base.sliderTheme,
|
||||
popupMenuTheme: const PopupMenuThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
color: Colors.white,
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor: Colors.indigo.withOpacity(0.15),
|
||||
iconTheme: WidgetStatePropertyAll(
|
||||
IconThemeData(color: Colors.grey[700]),
|
||||
),
|
||||
backgroundColor: immichBackgroundColor,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
labelTextStyle: WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
dialogTheme: const DialogTheme(
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.indigo,
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
textSelectionTheme: const TextSelectionThemeData(
|
||||
cursorColor: Colors.indigo,
|
||||
),
|
||||
);
|
||||
debugPrint("Current theme preset $primaryColorName");
|
||||
|
||||
final ThemeData immichDarkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.indigo,
|
||||
primaryColor: immichDarkThemePrimaryColor,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: immichDarkThemePrimaryColor,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
scaffoldBackgroundColor: immichDarkBackgroundColor,
|
||||
hintColor: Colors.grey[600],
|
||||
fontFamily: 'Overpass',
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
contentTextStyle: const TextStyle(
|
||||
fontFamily: 'Overpass',
|
||||
color: immichDarkThemePrimaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
try {
|
||||
return ImmichColorPreset.values
|
||||
.firstWhere((e) => e.name == primaryColorName);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"Theme preset $primaryColorName not found. Applying default preset.",
|
||||
);
|
||||
appSettingsProvider.setSetting(
|
||||
AppSettingsEnum.primaryColor,
|
||||
defaultColorPresetName,
|
||||
);
|
||||
return defaultColorPreset;
|
||||
}
|
||||
});
|
||||
|
||||
final dynamicThemeSettingProvider = StateProvider<bool>((ref) {
|
||||
return ref
|
||||
.watch(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.dynamicTheme);
|
||||
});
|
||||
|
||||
final colorfulInterfaceSettingProvider = StateProvider<bool>((ref) {
|
||||
return ref
|
||||
.watch(appSettingsServiceProvider)
|
||||
.getSetting(AppSettingsEnum.colorfulInterface);
|
||||
});
|
||||
|
||||
// Provider for current selected theme
|
||||
final immichThemeProvider = StateProvider<ImmichTheme>((ref) {
|
||||
var primaryColor = ref.read(immichThemePresetProvider);
|
||||
var useSystemColor = ref.watch(dynamicThemeSettingProvider);
|
||||
var useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider);
|
||||
|
||||
var currentTheme = (useSystemColor && _immichDynamicTheme != null)
|
||||
? _immichDynamicTheme!
|
||||
: primaryColor.getTheme();
|
||||
|
||||
return useColorfulInterface
|
||||
? currentTheme
|
||||
: _decolorizeSurfaces(theme: currentTheme);
|
||||
});
|
||||
|
||||
// Method to fetch dynamic system colors
|
||||
Future<void> fetchSystemPalette() async {
|
||||
try {
|
||||
final corePalette = await DynamicColorPlugin.getCorePalette();
|
||||
if (corePalette != null) {
|
||||
final primaryColor = corePalette.toColorScheme().primary;
|
||||
debugPrint('dynamic_color: Core palette detected.');
|
||||
|
||||
// Some palettes do not generate surface container colors accurately,
|
||||
// so we regenerate all colors using the primary color
|
||||
_immichDynamicTheme = ImmichTheme(
|
||||
light: ColorScheme.fromSeed(
|
||||
seedColor: primaryColor,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: primaryColor,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('dynamic_color: Failed to obtain core palette.');
|
||||
}
|
||||
}
|
||||
|
||||
// This method replaces all surface shades in ImmichTheme to a static ones
|
||||
// as we are creating the colorscheme through seedColor the default surfaces are
|
||||
// tinted with primary color
|
||||
ImmichTheme _decolorizeSurfaces({
|
||||
required ImmichTheme theme,
|
||||
}) {
|
||||
return ImmichTheme(
|
||||
light: theme.light.copyWith(
|
||||
surface: const Color(0xFFf9f9f9),
|
||||
onSurface: const Color(0xFF1b1b1b),
|
||||
surfaceContainerLowest: const Color(0xFFffffff),
|
||||
surfaceContainerLow: const Color(0xFFf3f3f3),
|
||||
surfaceContainer: const Color(0xFFeeeeee),
|
||||
surfaceContainerHigh: const Color(0xFFe8e8e8),
|
||||
surfaceContainerHighest: const Color(0xFFe2e2e2),
|
||||
surfaceDim: const Color(0xFFdadada),
|
||||
surfaceBright: const Color(0xFFf9f9f9),
|
||||
onSurfaceVariant: const Color(0xFF4c4546),
|
||||
inverseSurface: const Color(0xFF303030),
|
||||
onInverseSurface: const Color(0xFFf1f1f1),
|
||||
),
|
||||
backgroundColor: Colors.grey[900],
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: immichDarkThemePrimaryColor,
|
||||
dark: theme.dark.copyWith(
|
||||
surface: const Color(0xFF131313),
|
||||
onSurface: const Color(0xFFE2E2E2),
|
||||
surfaceContainerLowest: const Color(0xFF0E0E0E),
|
||||
surfaceContainerLow: const Color(0xFF1B1B1B),
|
||||
surfaceContainer: const Color(0xFF1F1F1F),
|
||||
surfaceContainerHigh: const Color(0xFF242424),
|
||||
surfaceContainerHighest: const Color(0xFF2E2E2E),
|
||||
surfaceDim: const Color(0xFF131313),
|
||||
surfaceBright: const Color(0xFF353535),
|
||||
onSurfaceVariant: const Color(0xFFCfC4C5),
|
||||
inverseSurface: const Color(0xFFE2E2E2),
|
||||
onInverseSurface: const Color(0xFF303030),
|
||||
),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Overpass',
|
||||
color: immichDarkThemePrimaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData getThemeData({required ColorScheme colorScheme}) {
|
||||
var isDark = colorScheme.brightness == Brightness.dark;
|
||||
var primaryColor = colorScheme.primary;
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: isDark ? Brightness.dark : Brightness.light,
|
||||
colorScheme: colorScheme,
|
||||
primaryColor: primaryColor,
|
||||
hintColor: colorScheme.onSurfaceSecondary,
|
||||
focusColor: primaryColor,
|
||||
scaffoldBackgroundColor: colorScheme.surface,
|
||||
splashColor: primaryColor.withOpacity(0.1),
|
||||
highlightColor: primaryColor.withOpacity(0.1),
|
||||
dialogBackgroundColor: colorScheme.surfaceContainer,
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: colorScheme.surfaceContainer,
|
||||
),
|
||||
backgroundColor: Color.fromARGB(255, 32, 33, 35),
|
||||
foregroundColor: immichDarkThemePrimaryColor,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Color.fromARGB(255, 35, 36, 37),
|
||||
selectedItemColor: immichDarkThemePrimaryColor,
|
||||
),
|
||||
drawerTheme: DrawerThemeData(
|
||||
backgroundColor: immichDarkBackgroundColor,
|
||||
scrimColor: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 255, 255, 255),
|
||||
fontFamily: 'Overpass',
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
contentTextStyle: TextStyle(
|
||||
fontFamily: 'Overpass',
|
||||
color: primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 255, 255, 255),
|
||||
appBarTheme: AppBarTheme(
|
||||
titleTextStyle: TextStyle(
|
||||
color: primaryColor,
|
||||
fontFamily: 'Overpass',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
backgroundColor:
|
||||
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
|
||||
foregroundColor: primaryColor,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: immichDarkThemePrimaryColor,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
cardColor: Colors.grey[900],
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.black87,
|
||||
backgroundColor: immichDarkThemePrimaryColor,
|
||||
),
|
||||
),
|
||||
chipTheme: base.chipTheme,
|
||||
sliderTheme: base.sliderTheme,
|
||||
popupMenuTheme: const PopupMenuThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4),
|
||||
iconTheme: WidgetStatePropertyAll(
|
||||
IconThemeData(color: Colors.grey[500]),
|
||||
),
|
||||
backgroundColor: Colors.grey[900],
|
||||
surfaceTintColor: Colors.transparent,
|
||||
labelTextStyle: WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[300],
|
||||
textTheme: TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : primaryColor,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
titleSmall: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleMedium: const TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
titleLarge: const TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
dialogTheme: const DialogTheme(
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: immichDarkThemePrimaryColor,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: isDark ? Colors.black87 : Colors.white,
|
||||
),
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
color: immichDarkThemePrimaryColor,
|
||||
chipTheme: const ChipThemeData(
|
||||
side: BorderSide.none,
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
sliderTheme: const SliderThemeData(
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
trackHeight: 2.0,
|
||||
),
|
||||
),
|
||||
textSelectionTheme: const TextSelectionThemeData(
|
||||
cursorColor: immichDarkThemePrimaryColor,
|
||||
),
|
||||
);
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
),
|
||||
popupMenuTheme: const PopupMenuThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor:
|
||||
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
|
||||
labelTextStyle: const WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
color: primaryColor,
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class AlbumActionOutlinedButton extends StatelessWidget {
|
||||
class AlbumActionFilledButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final String labelText;
|
||||
final IconData iconData;
|
||||
|
||||
const AlbumActionOutlinedButton({
|
||||
const AlbumActionFilledButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
required this.labelText,
|
||||
@@ -17,18 +17,13 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
side: BorderSide(
|
||||
width: 1,
|
||||
color: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 63, 63, 63)
|
||||
: const Color.fromARGB(255, 206, 206, 206),
|
||||
),
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
icon: Icon(
|
||||
iconData,
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
class AlbumThumbnailCard extends StatelessWidget {
|
||||
@@ -23,8 +24,6 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var isDarkTheme = context.isDarkTheme;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var cardSize = constraints.maxWidth;
|
||||
@@ -34,12 +33,13 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
height: cardSize,
|
||||
width: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[800] : Colors.grey[200],
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.no_photography,
|
||||
size: cardSize * .15,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -65,6 +65,9 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
return RichText(
|
||||
overflow: TextOverflow.fade,
|
||||
text: TextSpan(
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: album.assetCount == 1
|
||||
@@ -72,14 +75,9 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
.tr(args: ['${album.assetCount}'])
|
||||
: 'album_thumbnail_card_items'
|
||||
.tr(args: ['${album.assetCount}']),
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
if (owner != null) const TextSpan(text: ' · '),
|
||||
if (owner != null)
|
||||
TextSpan(
|
||||
text: owner,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
if (owner != null) TextSpan(text: owner),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -112,7 +110,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
album.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.primaryColor,
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,8 +20,6 @@ class AlbumTitleTextField extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
return TextField(
|
||||
onChanged: (v) {
|
||||
if (v.isEmpty) {
|
||||
@@ -35,7 +33,7 @@ class AlbumTitleTextField extends ConsumerWidget {
|
||||
focusNode: albumTitleTextFieldFocusNode,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
controller: albumTitleController,
|
||||
@@ -70,15 +68,12 @@ class AlbumTitleTextField extends ConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
hintText: 'share_add_title'.tr(),
|
||||
hintStyle: TextStyle(
|
||||
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
|
||||
fontSize: 28,
|
||||
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
focusColor: Colors.grey[300],
|
||||
fillColor: isDarkTheme
|
||||
? const Color.fromARGB(255, 32, 33, 35)
|
||||
: Colors.grey[200],
|
||||
fillColor: context.colorScheme.surfaceContainerHigh,
|
||||
filled: isAlbumTitleTextFieldFocus.value,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -95,7 +95,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
'action_common_confirm',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: !context.isDarkTheme ? Colors.red : Colors.red[300],
|
||||
color: context.colorScheme.error,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
|
||||
@@ -73,24 +73,18 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||
splashRadius: 10,
|
||||
)
|
||||
: null,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusColor: Colors.grey[300],
|
||||
fillColor: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 32, 33, 35)
|
||||
: Colors.grey[200],
|
||||
fillColor: context.scaffoldBackgroundColor,
|
||||
filled: titleFocusNode.hasFocus,
|
||||
hintText: 'share_add_title'.tr(),
|
||||
hintStyle: TextStyle(
|
||||
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
|
||||
fontSize: 28,
|
||||
color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -281,7 +281,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||
ScrollController scrollController,
|
||||
) {
|
||||
return Card(
|
||||
color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100],
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 18.0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
|
||||
@@ -22,12 +22,15 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => onPressed(),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
icon: Icon(
|
||||
Icons.close_rounded,
|
||||
color: context.colorScheme.onPrimary,
|
||||
),
|
||||
label: Text(
|
||||
'$selectedItemCount',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
height: 2.5,
|
||||
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
color: context.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
@@ -74,9 +75,9 @@ class GroupDividerTitle extends HookConsumerWidget {
|
||||
Icons.check_circle_rounded,
|
||||
color: context.primaryColor,
|
||||
)
|
||||
: const Icon(
|
||||
: Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
color: Colors.grey,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart';
|
||||
@@ -266,7 +267,9 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
scrollStateListener: dragScrolling,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
controller: _itemScrollController,
|
||||
backgroundColor: context.themeData.hintColor,
|
||||
backgroundColor: context.isDarkTheme
|
||||
? context.colorScheme.primary.darken(amount: .5)
|
||||
: context.colorScheme.primary,
|
||||
labelTextBuilder: _labelBuilder,
|
||||
padding: appBarOffset()
|
||||
? const EdgeInsets.only(top: 60)
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
@@ -42,8 +43,8 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetContainerColor = context.isDarkTheme
|
||||
? Colors.blueGrey
|
||||
: context.themeData.primaryColorLight;
|
||||
? context.primaryColor.darken(amount: 0.6)
|
||||
: context.primaryColor.lighten(amount: 0.8);
|
||||
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
|
||||
final isFromDto = asset.id == Isar.autoIncrement;
|
||||
|
||||
@@ -192,8 +193,8 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
bottom: 5,
|
||||
child: Icon(
|
||||
storageIcon(asset),
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
color: Colors.white.withOpacity(.8),
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class ThumbnailPlaceholder extends StatelessWidget {
|
||||
final EdgeInsets margin;
|
||||
@@ -13,25 +14,20 @@ class ThumbnailPlaceholder extends StatelessWidget {
|
||||
this.height = 250,
|
||||
});
|
||||
|
||||
static const _brightColors = [
|
||||
Color(0xFFF1F3F4),
|
||||
Color(0xFFB4B6B8),
|
||||
];
|
||||
|
||||
static const _darkColors = [
|
||||
Color(0xFF3B3F42),
|
||||
Color(0xFF2B2F32),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var gradientColors = [
|
||||
context.colorScheme.surfaceContainer,
|
||||
context.colorScheme.surfaceContainer.darken(amount: .1),
|
||||
];
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: context.isDarkTheme ? _darkColors : _brightColors,
|
||||
colors: gradientColors,
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/asset_description.service.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -23,7 +24,6 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
final controller = useTextEditingController();
|
||||
final focusNode = useFocusNode();
|
||||
final isFocus = useState(false);
|
||||
@@ -71,7 +71,7 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.cancel_rounded,
|
||||
color: Colors.grey[500],
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
splashRadius: 10,
|
||||
);
|
||||
@@ -100,9 +100,6 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
hintText: 'description_input_hint_text'.tr(),
|
||||
border: InputBorder.none,
|
||||
hintStyle: context.textTheme.labelLarge?.copyWith(
|
||||
color: textColor.withOpacity(0.5),
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||
var textColor = context.colorScheme.onSurface;
|
||||
final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
||||
// Format the date time with the timezone
|
||||
final (dt, timeZone) =
|
||||
|
||||
@@ -178,6 +178,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
actionsIconTheme: const IconThemeData(
|
||||
size: iconSize,
|
||||
),
|
||||
shape: const Border(),
|
||||
actions: [
|
||||
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
||||
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
|
||||
|
||||
@@ -47,22 +47,22 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
||||
|
||||
buildIcon() {
|
||||
if (isSelected) {
|
||||
return const Icon(
|
||||
return Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: Colors.green,
|
||||
color: context.colorScheme.primary,
|
||||
);
|
||||
}
|
||||
|
||||
if (isExcluded) {
|
||||
return const Icon(
|
||||
return Icon(
|
||||
Icons.remove_circle_rounded,
|
||||
color: Colors.red,
|
||||
color: context.colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
return Icon(
|
||||
Icons.circle,
|
||||
color: context.isDarkTheme ? Colors.grey[400] : Colors.black45,
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class BackupInfoCard extends StatelessWidget {
|
||||
final String title;
|
||||
@@ -19,9 +20,7 @@ class BackupInfoCard extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20), // if you need this
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? const Color.fromARGB(255, 56, 56, 56)
|
||||
: Colors.black12,
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -38,7 +37,9 @@ class BackupInfoCard extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(top: 4.0, right: 18.0),
|
||||
child: Text(
|
||||
subtitle,
|
||||
style: context.textTheme.bodyMedium,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: Column(
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
@@ -82,22 +83,20 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
Widget buildAssetInfoTable() {
|
||||
return Table(
|
||||
border: TableBorder.all(
|
||||
color: context.themeData.primaryColorLight,
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(
|
||||
// color: Colors.grey[100],
|
||||
),
|
||||
children: [
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'backup_controller_page_filename',
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
@@ -109,17 +108,15 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(
|
||||
// color: Colors.grey[200],
|
||||
),
|
||||
children: [
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"backup_controller_page_created",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
@@ -131,16 +128,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(
|
||||
// color: Colors.grey[100],
|
||||
),
|
||||
children: [
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"backup_controller_page_id",
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
@@ -181,8 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: uploadProgress / 100.0,
|
||||
backgroundColor: Colors.grey,
|
||||
color: context.primaryColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -214,8 +208,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10.0,
|
||||
value: uploadProgress / 100.0,
|
||||
backgroundColor: Colors.grey,
|
||||
color: context.primaryColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
|
||||
@@ -88,7 +88,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
|
||||
buildSettingButton() {
|
||||
return buildActionButton(
|
||||
Icons.settings_rounded,
|
||||
Icons.settings_outlined,
|
||||
"profile_drawer_settings",
|
||||
() => context.pushRoute(const SettingsRoute()),
|
||||
);
|
||||
@@ -146,9 +146,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: context.isDarkTheme
|
||||
? context.scaffoldBackgroundColor
|
||||
: const Color.fromARGB(255, 225, 229, 240),
|
||||
color: context.colorScheme.surface,
|
||||
),
|
||||
child: ListTile(
|
||||
minLeadingWidth: 50,
|
||||
@@ -171,10 +169,10 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 5.0,
|
||||
minHeight: 10.0,
|
||||
value: percentage,
|
||||
backgroundColor: Colors.grey,
|
||||
color: theme.primaryColor,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -248,7 +246,6 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
right: horizontalPadding,
|
||||
bottom: isHorizontal ? 20 : 100,
|
||||
),
|
||||
backgroundColor: theme.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user