mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/search-filter-album/web
This commit is contained in:
@@ -74,7 +74,7 @@ install_dependencies() {
|
|||||||
(
|
(
|
||||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||||
export CI=1 FROZEN=1 OFFLINE=1
|
export CI=1 FROZEN=1 OFFLINE=1
|
||||||
run_cmd make setup-dev
|
run_cmd make setup-web-dev setup-server-dev
|
||||||
)
|
)
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,17 +122,17 @@ jobs:
|
|||||||
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
||||||
run: |
|
run: |
|
||||||
if [[ $IS_MAIN == 'true' ]]; then
|
if [[ $IS_MAIN == 'true' ]]; then
|
||||||
flutter build apk --release
|
flutter build apk --release --flavor production
|
||||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||||
else
|
else
|
||||||
flutter build apk --debug --split-per-abi --target-platform android-arm64
|
flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish Android Artifact
|
- name: Publish Android Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: release-apk-signed
|
name: release-apk-signed
|
||||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
path: mobile/build/app/outputs/flutter-apk/**/*.apk
|
||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
id: cache-gradle-save
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: Org Checks
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_review:
|
pull_request_review:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-approvals:
|
check-approvals:
|
||||||
|
|||||||
Vendored
+19
@@ -18,6 +18,25 @@
|
|||||||
"name": "Immich Workers",
|
"name": "Immich Workers",
|
||||||
"remoteRoot": "/usr/src/app",
|
"remoteRoot": "/usr/src/app",
|
||||||
"localRoot": "${workspaceFolder}/server"
|
"localRoot": "${workspaceFolder}/server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flavor - Production",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"codeLens": {
|
||||||
|
"for": [
|
||||||
|
"run-test",
|
||||||
|
"run-test-file",
|
||||||
|
"run-file",
|
||||||
|
"debug-test",
|
||||||
|
"debug-test-file",
|
||||||
|
"debug-file",
|
||||||
|
],
|
||||||
|
"title": "${debugType}",
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
"--flavor", "production"
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ test-medium-dev:
|
|||||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||||
|
|
||||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||||
@@ -106,4 +106,5 @@ clean:
|
|||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||||
|
|
||||||
setup-dev: install-server install-sdk build-sdk install-web
|
setup-server-dev: install-server
|
||||||
|
setup-web-dev: install-sdk build-sdk install-web
|
||||||
|
|||||||
Generated
+22
-22
@@ -42,7 +42,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"vite": "^6.0.0",
|
"vite": "^7.0.0",
|
||||||
"vite-tsconfig-paths": "^5.0.0",
|
"vite-tsconfig-paths": "^5.0.0",
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
@@ -3466,9 +3466,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3486,7 +3486,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -4196,24 +4196,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
||||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.6",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.34.9",
|
"rollup": "^4.40.0",
|
||||||
"tinyglobby": "^0.2.13"
|
"tinyglobby": "^0.2.14"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
@@ -4222,14 +4222,14 @@
|
|||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "*",
|
"less": "^4.0.0",
|
||||||
"lightningcss": "^1.21.0",
|
"lightningcss": "^1.21.0",
|
||||||
"sass": "*",
|
"sass": "^1.70.0",
|
||||||
"sass-embedded": "*",
|
"sass-embedded": "^1.70.0",
|
||||||
"stylus": "*",
|
"stylus": ">=0.54.8",
|
||||||
"sugarss": "*",
|
"sugarss": "^5.0.0",
|
||||||
"terser": "^5.16.0",
|
"terser": "^5.16.0",
|
||||||
"tsx": "^4.8.1",
|
"tsx": "^4.8.1",
|
||||||
"yaml": "^2.4.2"
|
"yaml": "^2.4.2"
|
||||||
@@ -4314,9 +4314,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/fdir": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.4.4",
|
"version": "6.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"vite": "^6.0.0",
|
"vite": "^7.0.0",
|
||||||
"vite-tsconfig-paths": "^5.0.0",
|
"vite-tsconfig-paths": "^5.0.0",
|
||||||
"vitest": "^3.0.0",
|
"vitest": "^3.0.0",
|
||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -46,6 +46,12 @@ services:
|
|||||||
|
|
||||||
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
|
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
|
||||||
|
|
||||||
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
|
|
||||||
|
|
||||||
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
|
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
|
||||||
|
|
||||||
|
Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks).
|
||||||
|
|
||||||
|
<img src={require('./img/admin-nightly-tasks.webp').default} width="60%" title="Admin nightly tasks" />
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.
|
||||||
|
:::
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ In the Immich web UI:
|
|||||||
- Click Add path
|
- Click Add path
|
||||||
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
|
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
|
||||||
|
|
||||||
- Enter **/usr/src/app/external** as the path and click Add
|
- Enter **/home/user/photos1** as the path and click Add
|
||||||
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
|
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
|
||||||
|
|
||||||
- Save the new path
|
- Save the new path
|
||||||
|
|||||||
Generated
+1
@@ -14,6 +14,7 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.15.33",
|
"@types/node": "^22.15.33",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.15.33",
|
"@types/node": "^22.15.33",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ReactionType,
|
ReactionType,
|
||||||
createActivity as create,
|
createActivity as create,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
|
removeAssetFromAlbum,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -342,5 +343,36 @@ describe('/activities', () => {
|
|||||||
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return empty list when asset is removed', async () => {
|
||||||
|
const album3 = await createAlbum(
|
||||||
|
{
|
||||||
|
createAlbumDto: {
|
||||||
|
albumName: 'Album 3',
|
||||||
|
assetIds: [asset.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
|
);
|
||||||
|
|
||||||
|
await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like });
|
||||||
|
|
||||||
|
await removeAssetFromAlbum(
|
||||||
|
{
|
||||||
|
id: album3.id,
|
||||||
|
bulkIdsDto: {
|
||||||
|
ids: [asset.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/activities')
|
||||||
|
.query({ albumId: album.id })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['api_keys']);
|
await utils.resetDatabase(['api_key']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /api-keys', () => {
|
describe('POST /api-keys', () => {
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ describe('/system-config', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /system-config', () => {
|
describe('PUT /system-config', () => {
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).put('/system-config');
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should always return the new config', async () => {
|
it('should always return the new config', async () => {
|
||||||
const config = await getSystemConfig(admin.accessToken);
|
const config = await getSystemConfig(admin.accessToken);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('/tags', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// tagging assets eventually triggers metadata extraction which can impact other tests
|
// tagging assets eventually triggers metadata extraction which can impact other tests
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
await utils.resetDatabase(['tags']);
|
await utils.resetDatabase(['tag']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /tags', () => {
|
describe('POST /tags', () => {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['assets', 'albums']);
|
await utils.resetDatabase(['asset', 'album']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`immich upload /path/to/file.jpg`, () => {
|
describe(`immich upload /path/to/file.jpg`, () => {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export const deviceDto = {
|
|||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
updatedAt: expect.any(String),
|
updatedAt: expect.any(String),
|
||||||
current: true,
|
current: true,
|
||||||
|
isPendingSyncReset: false,
|
||||||
deviceOS: '',
|
deviceOS: '',
|
||||||
deviceType: '',
|
deviceType: '',
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-12
@@ -154,19 +154,19 @@ export const utils = {
|
|||||||
|
|
||||||
tables = tables || [
|
tables = tables || [
|
||||||
// TODO e2e test for deleting a stack, since it is quite complex
|
// TODO e2e test for deleting a stack, since it is quite complex
|
||||||
'asset_stack',
|
'stack',
|
||||||
'libraries',
|
'library',
|
||||||
'shared_links',
|
'shared_link',
|
||||||
'person',
|
'person',
|
||||||
'albums',
|
'album',
|
||||||
'assets',
|
'asset',
|
||||||
'asset_faces',
|
'asset_face',
|
||||||
'activity',
|
'activity',
|
||||||
'api_keys',
|
'api_key',
|
||||||
'sessions',
|
'session',
|
||||||
'users',
|
'user',
|
||||||
'system_metadata',
|
'system_metadata',
|
||||||
'tags',
|
'tag',
|
||||||
];
|
];
|
||||||
|
|
||||||
const sql: string[] = [];
|
const sql: string[] = [];
|
||||||
@@ -175,7 +175,7 @@ export const utils = {
|
|||||||
if (table === 'system_metadata') {
|
if (table === 'system_metadata') {
|
||||||
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
||||||
} else {
|
} else {
|
||||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
sql.push(`DELETE FROM "${table}" CASCADE;`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +451,7 @@ export const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
||||||
},
|
},
|
||||||
|
|
||||||
setPersonThumbnail: async (personId: string) => {
|
setPersonThumbnail: async (personId: string) => {
|
||||||
|
|||||||
@@ -166,6 +166,20 @@
|
|||||||
"metadata_settings_description": "Manage metadata settings",
|
"metadata_settings_description": "Manage metadata settings",
|
||||||
"migration_job": "Migration",
|
"migration_job": "Migration",
|
||||||
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure",
|
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure",
|
||||||
|
"nightly_tasks_cluster_faces_setting_description": "Run facial recognition on newly detected faces",
|
||||||
|
"nightly_tasks_cluster_new_faces_setting": "Cluster new faces",
|
||||||
|
"nightly_tasks_database_cleanup_setting": "Database cleanup tasks",
|
||||||
|
"nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database",
|
||||||
|
"nightly_tasks_generate_memories_setting": "Generate memories",
|
||||||
|
"nightly_tasks_generate_memories_setting_description": "Create new memories from assets",
|
||||||
|
"nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails",
|
||||||
|
"nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation",
|
||||||
|
"nightly_tasks_settings": "Nightly Tasks Settings",
|
||||||
|
"nightly_tasks_settings_description": "Manage nightly tasks",
|
||||||
|
"nightly_tasks_start_time_setting": "Start time",
|
||||||
|
"nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks",
|
||||||
|
"nightly_tasks_sync_quota_usage_setting": "Sync quota usage",
|
||||||
|
"nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage",
|
||||||
"no_paths_added": "No paths added",
|
"no_paths_added": "No paths added",
|
||||||
"no_pattern_added": "No pattern added",
|
"no_pattern_added": "No pattern added",
|
||||||
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||||
@@ -1502,6 +1516,7 @@
|
|||||||
"remove_custom_date_range": "Remove custom date range",
|
"remove_custom_date_range": "Remove custom date range",
|
||||||
"remove_deleted_assets": "Remove Deleted Assets",
|
"remove_deleted_assets": "Remove Deleted Assets",
|
||||||
"remove_from_album": "Remove from album",
|
"remove_from_album": "Remove from album",
|
||||||
|
"remove_from_album_action_prompt": "{count} removed from the album",
|
||||||
"remove_from_favorites": "Remove from favorites",
|
"remove_from_favorites": "Remove from favorites",
|
||||||
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
|
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
|
||||||
"remove_from_locked_folder": "Remove from locked folder",
|
"remove_from_locked_folder": "Remove from locked folder",
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.29.3"
|
"flutter": "3.32.6"
|
||||||
}
|
}
|
||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
|
"dart.flutterSdkPath": ".fvm/versions/3.32.6",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,6 +66,20 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "default"
|
||||||
|
productFlavors {
|
||||||
|
production {
|
||||||
|
dimension "default"
|
||||||
|
applicationId "app.alextran.immich"
|
||||||
|
}
|
||||||
|
|
||||||
|
beta {
|
||||||
|
dimension "default"
|
||||||
|
applicationId "app.alextran.immich.beta"
|
||||||
|
versionNameSuffix "-BETA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<application android:label="Immich Beta" tools:replace="android:label" />
|
||||||
|
</manifest>
|
||||||
@@ -100,24 +100,24 @@
|
|||||||
|
|
||||||
<!-- my.immich.app deep link -->
|
<!-- my.immich.app deep link -->
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:path="/" />
|
android:path="/" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/albums/" />
|
android:pathPrefix="/albums/" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/memories/" />
|
android:pathPrefix="/memories/" />
|
||||||
<data
|
<data
|
||||||
android:host="my.immich.app"
|
android:host="my.immich.app"
|
||||||
android:pathPrefix="/photos/" />
|
android:pathPrefix="/photos/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "80.0.0"
|
version: "82.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
version: "7.4.5"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
|
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.13.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -53,10 +53,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: checked_yaml
|
name: checked_yaml
|
||||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.4"
|
||||||
ci:
|
ci:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -125,10 +125,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_visitor
|
name: custom_lint_visitor
|
||||||
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
|
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0+7.3.0"
|
version: "1.0.0+7.4.5"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -157,10 +157,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: freezed_annotation
|
name: freezed_annotation
|
||||||
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.1.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -213,18 +213,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_config
|
name: package_config
|
||||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -317,10 +317,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.6"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -341,18 +341,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "15.0.2"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -362,4 +362,4 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -117,8 +117,6 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -473,10 +471,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -505,10 +507,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ enum AlbumUserRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model for an album stored in the server
|
// Model for an album stored in the server
|
||||||
class Album {
|
class RemoteAlbum {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
@@ -24,7 +24,7 @@ class Album {
|
|||||||
final int assetCount;
|
final int assetCount;
|
||||||
final String ownerName;
|
final String ownerName;
|
||||||
|
|
||||||
const Album({
|
const RemoteAlbum({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
@@ -57,7 +57,7 @@ class Album {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! Album) return false;
|
if (other is! RemoteAlbum) return false;
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
return id == other.id &&
|
return id == other.id &&
|
||||||
name == other.name &&
|
name == other.name &&
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||||
|
|
||||||
class RemoteAlbumService {
|
class RemoteAlbumService {
|
||||||
final DriftRemoteAlbumRepository _repository;
|
final DriftRemoteAlbumRepository _repository;
|
||||||
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
|
||||||
const RemoteAlbumService(this._repository);
|
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||||
|
|
||||||
Future<List<Album>> getAll() {
|
Future<List<RemoteAlbum>> getAll() {
|
||||||
return _repository.getAll();
|
return _repository.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Album> sortAlbums(
|
List<RemoteAlbum> sortAlbums(
|
||||||
List<Album> albums,
|
List<RemoteAlbum> albums,
|
||||||
RemoteAlbumSortMode sortMode, {
|
RemoteAlbumSortMode sortMode, {
|
||||||
bool isReverse = false,
|
bool isReverse = false,
|
||||||
}) {
|
}) {
|
||||||
return sortMode.sortFn(albums, isReverse);
|
return sortMode.sortFn(albums, isReverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Album> searchAlbums(
|
List<RemoteAlbum> searchAlbums(
|
||||||
List<Album> albums,
|
List<RemoteAlbum> albums,
|
||||||
String query,
|
String query,
|
||||||
String? userId, [
|
String? userId, [
|
||||||
QuickFilterMode filterMode = QuickFilterMode.all,
|
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||||
]) {
|
]) {
|
||||||
final lowerQuery = query.toLowerCase();
|
final lowerQuery = query.toLowerCase();
|
||||||
List<Album> filtered = albums;
|
List<RemoteAlbum> filtered = albums;
|
||||||
|
|
||||||
// Apply text search filter
|
// Apply text search filter
|
||||||
if (query.isNotEmpty) {
|
if (query.isNotEmpty) {
|
||||||
@@ -57,4 +59,20 @@ class RemoteAlbumService {
|
|||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum> createAlbum({
|
||||||
|
required String title,
|
||||||
|
required List<String> assetIds,
|
||||||
|
String? description,
|
||||||
|
}) async {
|
||||||
|
final album = await _albumApiRepository.createDriftAlbum(
|
||||||
|
title,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _repository.create(album, assetIds);
|
||||||
|
|
||||||
|
return album;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -23,7 +24,12 @@ class SyncStreamService {
|
|||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> sync() => _syncApiRepository.streamChanges(_handleEvents);
|
Future<void> sync() {
|
||||||
|
_logger.info("Remote sync request for userr");
|
||||||
|
DLog.log("Remote sync request for user");
|
||||||
|
// Start the sync stream and handle events
|
||||||
|
return _syncApiRepository.streamChanges(_handleEvents);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
|
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
|
||||||
List<SyncEvent> items = [];
|
List<SyncEvent> items = [];
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ typedef TimelineAssetSource = Future<List<BaseAsset>> Function(
|
|||||||
|
|
||||||
typedef TimelineBucketSource = Stream<List<Bucket>> Function();
|
typedef TimelineBucketSource = Stream<List<Bucket>> Function();
|
||||||
|
|
||||||
|
typedef TimelineQuery = ({
|
||||||
|
TimelineAssetSource assetSource,
|
||||||
|
TimelineBucketSource bucketSource,
|
||||||
|
});
|
||||||
|
|
||||||
class TimelineFactory {
|
class TimelineFactory {
|
||||||
final DriftTimelineRepository _timelineRepository;
|
final DriftTimelineRepository _timelineRepository;
|
||||||
final SettingsService _settingsService;
|
final SettingsService _settingsService;
|
||||||
@@ -31,78 +36,32 @@ class TimelineFactory {
|
|||||||
GroupAssetsBy get groupBy =>
|
GroupAssetsBy get groupBy =>
|
||||||
GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
|
GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
|
||||||
|
|
||||||
TimelineService main(List<String> timelineUsers) => TimelineService(
|
TimelineService main(List<String> timelineUsers) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.main(timelineUsers, groupBy));
|
||||||
.getMainBucketAssets(timelineUsers, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchMainBucket(
|
|
||||||
timelineUsers,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService localAlbum({required String albumId}) => TimelineService(
|
TimelineService localAlbum({required String albumId}) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.localAlbum(albumId, groupBy));
|
||||||
.getLocalAlbumBucketAssets(albumId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchLocalAlbumBucket(
|
|
||||||
albumId,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService remoteAlbum({required String albumId}) => TimelineService(
|
TimelineService remoteAlbum({required String albumId}) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy));
|
||||||
.getRemoteAlbumBucketAssets(albumId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchRemoteAlbumBucket(
|
|
||||||
albumId,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService remoteAssets(String ownerId) => TimelineService(
|
TimelineService remoteAssets(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.remote(userId, groupBy));
|
||||||
.getRemoteBucketAssets(ownerId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchRemoteBucket(
|
|
||||||
ownerId,
|
|
||||||
groupBy: GroupAssetsBy.month,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService favorite(String userId) => TimelineService(
|
TimelineService favorite(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.favorite(userId, groupBy));
|
||||||
.getFavoriteBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService trash(String userId) => TimelineService(
|
TimelineService trash(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.trash(userId, groupBy));
|
||||||
.getTrashBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService archive(String userId) => TimelineService(
|
TimelineService archive(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.archived(userId, groupBy));
|
||||||
.getArchiveBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService lockedFolder(String userId) => TimelineService(
|
TimelineService lockedFolder(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.locked(userId, groupBy));
|
||||||
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
|
|
||||||
userId,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService video(String userId) => TimelineService(
|
TimelineService video(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.video(userId, groupBy));
|
||||||
.getVideoBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchVideoBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
@@ -116,7 +75,13 @@ class TimelineService {
|
|||||||
int _totalAssets = 0;
|
int _totalAssets = 0;
|
||||||
int get totalAssets => _totalAssets;
|
int get totalAssets => _totalAssets;
|
||||||
|
|
||||||
TimelineService({
|
TimelineService(TimelineQuery query)
|
||||||
|
: this._(
|
||||||
|
assetSource: query.assetSource,
|
||||||
|
bucketSource: query.bucketSource,
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService._({
|
||||||
required TimelineAssetSource assetSource,
|
required TimelineAssetSource assetSource,
|
||||||
required TimelineBucketSource bucketSource,
|
required TimelineBucketSource bucketSource,
|
||||||
}) : _assetSource = assetSource,
|
}) : _assetSource = assetSource,
|
||||||
@@ -210,6 +175,9 @@ class TimelineService {
|
|||||||
Future<void> preCacheAssets(int index) =>
|
Future<void> preCacheAssets(int index) =>
|
||||||
_mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index)));
|
_mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index)));
|
||||||
|
|
||||||
|
BaseAsset getRandomAsset() =>
|
||||||
|
_buffer.elementAt(math.Random().nextInt(_buffer.length));
|
||||||
|
|
||||||
BaseAsset getAsset(int index) {
|
BaseAsset getAsset(int index) {
|
||||||
if (!hasRange(index, 1)) {
|
if (!hasRange(index, 1)) {
|
||||||
throw RangeError(
|
throw RangeError(
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ class FastScrollPhysics extends ScrollPhysics {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
SpringDescription get spring => const SpringDescription(
|
SpringDescription get spring => const SpringDescription(
|
||||||
mass: 40,
|
mass: 1,
|
||||||
stiffness: 100,
|
stiffness: 402.49984375,
|
||||||
damping: 1,
|
damping: 40,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics {
|
|||||||
// can briefly be seen and cause a flicker effect if the video begins to initialize
|
// can briefly be seen and cause a flicker effect if the video begins to initialize
|
||||||
// before the animation finishes - probably a bug in PhotoViewGallery's animation handling
|
// before the animation finishes - probably a bug in PhotoViewGallery's animation handling
|
||||||
// Making the animation faster is not just stylistic, but also helps to avoid this flicker
|
// Making the animation faster is not just stylistic, but also helps to avoid this flicker
|
||||||
mass: 80,
|
mass: 1,
|
||||||
stiffness: 100,
|
stiffness: 1601.2499609375,
|
||||||
damping: 1,
|
damping: 80,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
enum SortRemoteAlbumsBy { id }
|
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||||
|
|
||||||
class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
||||||
|
|
||||||
Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
Future<List<RemoteAlbum>> getAll({
|
||||||
|
Set<SortRemoteAlbumsBy> sortBy = const {SortRemoteAlbumsBy.updatedAt},
|
||||||
|
}) {
|
||||||
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
||||||
|
|
||||||
final query = _db.remoteAlbumEntity.select().join([
|
final query = _db.remoteAlbumEntity.select().join([
|
||||||
@@ -41,6 +44,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
orderings.add(
|
orderings.add(
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
|
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
|
||||||
|
SortRemoteAlbumsBy.updatedAt =>
|
||||||
|
OrderingTerm.desc(_db.remoteAlbumEntity.updatedAt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,11 +61,54 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> create(
|
||||||
|
RemoteAlbum album,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
|
await _db.transaction(() async {
|
||||||
|
final entity = RemoteAlbumEntityCompanion(
|
||||||
|
id: Value(album.id),
|
||||||
|
name: Value(album.name),
|
||||||
|
ownerId: Value(album.ownerId),
|
||||||
|
createdAt: Value(album.createdAt),
|
||||||
|
updatedAt: Value(album.updatedAt),
|
||||||
|
description: Value(album.description),
|
||||||
|
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||||
|
isActivityEnabled: Value(album.isActivityEnabled),
|
||||||
|
order: Value(album.order),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _db.remoteAlbumEntity.insertOne(entity);
|
||||||
|
|
||||||
|
if (assetIds.isNotEmpty) {
|
||||||
|
final albumAssets = assetIds.map(
|
||||||
|
(assetId) => RemoteAlbumAssetEntityCompanion(
|
||||||
|
albumId: Value(album.id),
|
||||||
|
assetId: Value(assetId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
albumAssets,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> removeAssets(String albumId, List<String> assetIds) {
|
||||||
|
return _db.remoteAlbumAssetEntity.deleteWhere(
|
||||||
|
(tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
Album toDto({int assetCount = 0, required String ownerName}) {
|
RemoteAlbum toDto({int assetCount = 0, required String ownerName}) {
|
||||||
return Album(
|
return RemoteAlbum(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:stream_transform/stream_transform.dart';
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
|
||||||
@@ -30,19 +32,19 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.map((users) => users..add(userId));
|
.map((users) => users..add(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Bucket> _generateBuckets(int count) {
|
TimelineQuery main(List<String> userIds, GroupAssetsBy groupBy) => (
|
||||||
final numBuckets = (count / kTimelineNoneSegmentSize).floor();
|
bucketSource: () => _watchMainBucket(
|
||||||
final buckets = List.generate(
|
userIds,
|
||||||
numBuckets,
|
groupBy: groupBy,
|
||||||
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
|
),
|
||||||
);
|
assetSource: (offset, count) => _getMainBucketAssets(
|
||||||
if (count % kTimelineNoneSegmentSize != 0) {
|
userIds,
|
||||||
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
|
offset: offset,
|
||||||
}
|
count: count,
|
||||||
return buckets;
|
),
|
||||||
}
|
);
|
||||||
|
|
||||||
Stream<List<Bucket>> watchMainBucket(
|
Stream<List<Bucket>> _watchMainBucket(
|
||||||
List<String> userIds, {
|
List<String> userIds, {
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
}) {
|
}) {
|
||||||
@@ -62,7 +64,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.throttle(const Duration(seconds: 3), trailing: true);
|
.throttle(const Duration(seconds: 3), trailing: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> getMainBucketAssets(
|
Future<List<BaseAsset>> _getMainBucketAssets(
|
||||||
List<String> userIds, {
|
List<String> userIds, {
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
@@ -70,42 +72,53 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return _db.mergedAssetDrift
|
return _db.mergedAssetDrift
|
||||||
.mergedAsset(userIds, limit: Limit(count, offset))
|
.mergedAsset(userIds, limit: Limit(count, offset))
|
||||||
.map(
|
.map(
|
||||||
(row) {
|
(row) => row.remoteId != null && row.ownerId != null
|
||||||
return row.remoteId != null && row.ownerId != null
|
? RemoteAsset(
|
||||||
? RemoteAsset(
|
id: row.remoteId!,
|
||||||
id: row.remoteId!,
|
localId: row.localId,
|
||||||
localId: row.localId,
|
name: row.name,
|
||||||
name: row.name,
|
ownerId: row.ownerId!,
|
||||||
ownerId: row.ownerId!,
|
checksum: row.checksum,
|
||||||
checksum: row.checksum,
|
type: row.type,
|
||||||
type: row.type,
|
createdAt: row.createdAt,
|
||||||
createdAt: row.createdAt,
|
updatedAt: row.updatedAt,
|
||||||
updatedAt: row.updatedAt,
|
thumbHash: row.thumbHash,
|
||||||
thumbHash: row.thumbHash,
|
width: row.width,
|
||||||
width: row.width,
|
height: row.height,
|
||||||
height: row.height,
|
isFavorite: row.isFavorite,
|
||||||
isFavorite: row.isFavorite,
|
durationInSeconds: row.durationInSeconds,
|
||||||
durationInSeconds: row.durationInSeconds,
|
)
|
||||||
)
|
: LocalAsset(
|
||||||
: LocalAsset(
|
id: row.localId!,
|
||||||
id: row.localId!,
|
remoteId: row.remoteId,
|
||||||
remoteId: row.remoteId,
|
name: row.name,
|
||||||
name: row.name,
|
checksum: row.checksum,
|
||||||
checksum: row.checksum,
|
type: row.type,
|
||||||
type: row.type,
|
createdAt: row.createdAt,
|
||||||
createdAt: row.createdAt,
|
updatedAt: row.updatedAt,
|
||||||
updatedAt: row.updatedAt,
|
width: row.width,
|
||||||
width: row.width,
|
height: row.height,
|
||||||
height: row.height,
|
isFavorite: row.isFavorite,
|
||||||
isFavorite: row.isFavorite,
|
durationInSeconds: row.durationInSeconds,
|
||||||
durationInSeconds: row.durationInSeconds,
|
orientation: row.orientation,
|
||||||
orientation: row.orientation,
|
),
|
||||||
);
|
)
|
||||||
},
|
.get();
|
||||||
).get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchLocalAlbumBucket(
|
TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => (
|
||||||
|
bucketSource: () => _watchLocalAlbumBucket(
|
||||||
|
albumId,
|
||||||
|
groupBy: groupBy,
|
||||||
|
),
|
||||||
|
assetSource: (offset, count) => _getLocalAlbumBucketAssets(
|
||||||
|
albumId,
|
||||||
|
offset: offset,
|
||||||
|
count: count,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Stream<List<Bucket>> _watchLocalAlbumBucket(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
}) {
|
}) {
|
||||||
@@ -119,15 +132,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
final assetCountExp = _db.localAssetEntity.id.count();
|
final assetCountExp = _db.localAssetEntity.id.count();
|
||||||
final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy);
|
final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
final query = _db.localAssetEntity.selectOnly()
|
final query = _db.localAssetEntity.selectOnly().join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
..addColumns([assetCountExp, dateExp])
|
..addColumns([assetCountExp, dateExp])
|
||||||
..join([
|
|
||||||
innerJoin(
|
|
||||||
_db.localAlbumAssetEntity,
|
|
||||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
..groupBy([dateExp])
|
..groupBy([dateExp])
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
@@ -139,7 +151,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
}).watch();
|
}).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> getLocalAlbumBucketAssets(
|
Future<List<BaseAsset>> _getLocalAlbumBucketAssets(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
@@ -156,12 +168,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchRemoteAlbumBucket(
|
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
|
||||||
|
bucketSource: () => _watchRemoteAlbumBucket(
|
||||||
|
albumId,
|
||||||
|
groupBy: groupBy,
|
||||||
|
),
|
||||||
|
assetSource: (offset, count) => _getRemoteAlbumBucketAssets(
|
||||||
|
albumId,
|
||||||
|
offset: offset,
|
||||||
|
count: count,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Stream<List<Bucket>> _watchRemoteAlbumBucket(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
}) {
|
}) {
|
||||||
@@ -199,7 +224,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
}).watch();
|
}).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> getRemoteAlbumBucketAssets(
|
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
@@ -225,325 +250,101 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchRemoteBucket(
|
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
|
||||||
String ownerId, {
|
_remoteQueryBuilder(
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
filter: (row) =>
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
|
||||||
return _db.remoteAssetEntity
|
|
||||||
.count(
|
|
||||||
where: (row) =>
|
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline) &
|
|
||||||
row.ownerId.equals(ownerId),
|
|
||||||
)
|
|
||||||
.map(_generateBuckets)
|
|
||||||
.watchSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
|
||||||
..addColumns([assetCountExp, dateExp])
|
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
|
||||||
_db.remoteAssetEntity.visibility
|
|
||||||
.equalsValue(AssetVisibility.timeline) &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(ownerId),
|
|
||||||
)
|
|
||||||
..groupBy([dateExp])
|
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
|
||||||
final assetCount = row.read(assetCountExp)!;
|
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
|
||||||
}).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<BaseAsset>> getRemoteBucketAssets(
|
|
||||||
String ownerId, {
|
|
||||||
required int offset,
|
|
||||||
required int count,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
|
||||||
..where(
|
|
||||||
(row) =>
|
|
||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline) &
|
row.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
row.ownerId.equals(ownerId),
|
row.ownerId.equals(ownerId),
|
||||||
)
|
groupBy: groupBy,
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
);
|
||||||
..limit(count, offset: offset);
|
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) =>
|
||||||
}
|
_remoteQueryBuilder(
|
||||||
|
filter: (row) =>
|
||||||
Stream<List<Bucket>> watchFavoriteBucket(
|
|
||||||
String userId, {
|
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
|
||||||
return _db.remoteAssetEntity
|
|
||||||
.count(
|
|
||||||
where: (row) =>
|
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.isFavorite.equals(true) &
|
|
||||||
row.ownerId.equals(userId),
|
|
||||||
)
|
|
||||||
.map(_generateBuckets)
|
|
||||||
.watchSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
|
||||||
..addColumns([assetCountExp, dateExp])
|
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
_db.remoteAssetEntity.isFavorite.equals(true),
|
|
||||||
)
|
|
||||||
..groupBy([dateExp])
|
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
|
||||||
final assetCount = row.read(assetCountExp)!;
|
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
|
||||||
}).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<BaseAsset>> getFavoriteBucketAssets(
|
|
||||||
String userId, {
|
|
||||||
required int offset,
|
|
||||||
required int count,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
|
||||||
..where(
|
|
||||||
(row) =>
|
|
||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
row.isFavorite.equals(true) &
|
row.isFavorite.equals(true) &
|
||||||
row.ownerId.equals(userId),
|
row.ownerId.equals(userId),
|
||||||
)
|
groupBy: groupBy,
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
);
|
||||||
..limit(count, offset: offset);
|
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
TimelineQuery trash(String userId, GroupAssetsBy groupBy) =>
|
||||||
}
|
_remoteQueryBuilder(
|
||||||
|
filter: (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
||||||
|
groupBy: groupBy,
|
||||||
|
);
|
||||||
|
|
||||||
Stream<List<Bucket>> watchTrashBucket(
|
TimelineQuery archived(String userId, GroupAssetsBy groupBy) =>
|
||||||
String userId, {
|
_remoteQueryBuilder(
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
filter: (row) =>
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
|
||||||
return _db.remoteAssetEntity
|
|
||||||
.count(
|
|
||||||
where: (row) =>
|
|
||||||
row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
|
||||||
)
|
|
||||||
.map(_generateBuckets)
|
|
||||||
.watchSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
|
||||||
..addColumns([assetCountExp, dateExp])
|
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNotNull(),
|
|
||||||
)
|
|
||||||
..groupBy([dateExp])
|
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
|
||||||
final assetCount = row.read(assetCountExp)!;
|
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
|
||||||
}).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<BaseAsset>> getTrashBucketAssets(
|
|
||||||
String userId, {
|
|
||||||
required int offset,
|
|
||||||
required int count,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
|
||||||
..where(
|
|
||||||
(row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
|
||||||
)
|
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
|
||||||
..limit(count, offset: offset);
|
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<List<Bucket>> watchArchiveBucket(
|
|
||||||
String userId, {
|
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
|
||||||
return _db.remoteAssetEntity
|
|
||||||
.count(
|
|
||||||
where: (row) =>
|
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.visibility.equalsValue(AssetVisibility.archive) &
|
|
||||||
row.ownerId.equals(userId),
|
|
||||||
)
|
|
||||||
.map(_generateBuckets)
|
|
||||||
.watchSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
|
||||||
..addColumns([assetCountExp, dateExp])
|
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
_db.remoteAssetEntity.visibility
|
|
||||||
.equalsValue(AssetVisibility.archive),
|
|
||||||
)
|
|
||||||
..groupBy([dateExp])
|
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
|
||||||
final assetCount = row.read(assetCountExp)!;
|
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
|
||||||
}).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<BaseAsset>> getArchiveBucketAssets(
|
|
||||||
String userId, {
|
|
||||||
required int offset,
|
|
||||||
required int count,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
|
||||||
..where(
|
|
||||||
(row) =>
|
|
||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
row.ownerId.equals(userId) &
|
row.ownerId.equals(userId) &
|
||||||
row.visibility.equalsValue(AssetVisibility.archive),
|
row.visibility.equalsValue(AssetVisibility.archive),
|
||||||
)
|
groupBy: groupBy,
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
);
|
||||||
..limit(count, offset: offset);
|
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
TimelineQuery locked(String userId, GroupAssetsBy groupBy) =>
|
||||||
}
|
_remoteQueryBuilder(
|
||||||
|
filter: (row) =>
|
||||||
Stream<List<Bucket>> watchLockedFolderBucket(
|
|
||||||
String userId, {
|
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
|
||||||
return _db.remoteAssetEntity
|
|
||||||
.count(
|
|
||||||
where: (row) =>
|
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.visibility.equalsValue(AssetVisibility.locked) &
|
|
||||||
row.ownerId.equals(userId),
|
|
||||||
)
|
|
||||||
.map(_generateBuckets)
|
|
||||||
.watchSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
|
||||||
..addColumns([assetCountExp, dateExp])
|
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
_db.remoteAssetEntity.visibility
|
|
||||||
.equalsValue(AssetVisibility.locked),
|
|
||||||
)
|
|
||||||
..groupBy([dateExp])
|
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
|
||||||
final assetCount = row.read(assetCountExp)!;
|
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
|
||||||
}).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<BaseAsset>> getLockedFolderBucketAssets(
|
|
||||||
String userId, {
|
|
||||||
required int offset,
|
|
||||||
required int count,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
|
||||||
..where(
|
|
||||||
(row) =>
|
|
||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
row.visibility.equalsValue(AssetVisibility.locked) &
|
row.visibility.equalsValue(AssetVisibility.locked) &
|
||||||
row.ownerId.equals(userId),
|
row.ownerId.equals(userId),
|
||||||
)
|
groupBy: groupBy,
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
);
|
||||||
..limit(count, offset: offset);
|
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
TimelineQuery video(String userId, GroupAssetsBy groupBy) =>
|
||||||
}
|
_remoteQueryBuilder(
|
||||||
|
filter: (row) =>
|
||||||
Stream<List<Bucket>> watchVideoBucket(
|
|
||||||
String userId, {
|
|
||||||
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
|
||||||
}) {
|
|
||||||
if (groupBy == GroupAssetsBy.none) {
|
|
||||||
return _db.remoteAssetEntity
|
|
||||||
.count(
|
|
||||||
where: (row) =>
|
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.type.equalsValue(AssetType.video) &
|
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline) &
|
|
||||||
row.ownerId.equals(userId),
|
|
||||||
)
|
|
||||||
.map(_generateBuckets)
|
|
||||||
.watchSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetCountExp = _db.remoteAssetEntity.id.count();
|
|
||||||
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
|
||||||
..addColumns([assetCountExp, dateExp])
|
|
||||||
..where(
|
|
||||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
|
|
||||||
_db.remoteAssetEntity.visibility
|
|
||||||
.equalsValue(AssetVisibility.timeline),
|
|
||||||
)
|
|
||||||
..groupBy([dateExp])
|
|
||||||
..orderBy([OrderingTerm.desc(dateExp)]);
|
|
||||||
|
|
||||||
return query.map((row) {
|
|
||||||
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
|
||||||
final assetCount = row.read(assetCountExp)!;
|
|
||||||
return TimeBucket(date: timeline, assetCount: assetCount);
|
|
||||||
}).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<BaseAsset>> getVideoBucketAssets(
|
|
||||||
String userId, {
|
|
||||||
required int offset,
|
|
||||||
required int count,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
|
||||||
..where(
|
|
||||||
(row) =>
|
|
||||||
row.deletedAt.isNull() &
|
row.deletedAt.isNull() &
|
||||||
row.type.equalsValue(AssetType.video) &
|
row.type.equalsValue(AssetType.video) &
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline) &
|
row.visibility.equalsValue(AssetVisibility.timeline) &
|
||||||
row.ownerId.equals(userId),
|
row.ownerId.equals(userId),
|
||||||
)
|
groupBy: groupBy,
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineQuery _remoteQueryBuilder({
|
||||||
|
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy),
|
||||||
|
assetSource: (offset, count) =>
|
||||||
|
_getRemoteAssets(filter: filter, offset: offset, count: count),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> _watchRemoteBucket({
|
||||||
|
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
final query = _db.remoteAssetEntity.count(where: filter);
|
||||||
|
return query.map(_generateBuckets).watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(filter(_db.remoteAssetEntity))
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> _getRemoteAssets({
|
||||||
|
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(filter)
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
@@ -551,6 +352,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Bucket> _generateBuckets(int count) {
|
||||||
|
final buckets = List.generate(
|
||||||
|
(count / kTimelineNoneSegmentSize).floor(),
|
||||||
|
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
|
||||||
|
);
|
||||||
|
if (count % kTimelineNoneSegmentSize != 0) {
|
||||||
|
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
extension on Expression<DateTime> {
|
extension on Expression<DateTime> {
|
||||||
Expression<String> dateFmt(GroupAssetsBy groupBy) {
|
Expression<String> dateFmt(GroupAssetsBy groupBy) {
|
||||||
// DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting
|
// DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ import 'package:immich_mobile/providers/theme.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
|
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/download.dart';
|
import 'package:immich_mobile/utils/download.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
@@ -176,10 +176,13 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
final deepLinkHandler = ref.read(deepLinkServiceProvider);
|
final deepLinkHandler = ref.read(deepLinkServiceProvider);
|
||||||
final currentRouteName = ref.read(currentRouteNameProvider.notifier).state;
|
final currentRouteName = ref.read(currentRouteNameProvider.notifier).state;
|
||||||
|
|
||||||
|
final isColdStart =
|
||||||
|
currentRouteName == null || currentRouteName == SplashScreenRoute.name;
|
||||||
|
|
||||||
if (deepLink.uri.scheme == "immich") {
|
if (deepLink.uri.scheme == "immich") {
|
||||||
final proposedRoute = await deepLinkHandler.handleScheme(
|
final proposedRoute = await deepLinkHandler.handleScheme(
|
||||||
deepLink,
|
deepLink,
|
||||||
currentRouteName == SplashScreenRoute.name,
|
isColdStart,
|
||||||
);
|
);
|
||||||
|
|
||||||
return proposedRoute;
|
return proposedRoute;
|
||||||
@@ -188,7 +191,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
if (deepLink.uri.host == "my.immich.app") {
|
if (deepLink.uri.host == "my.immich.app") {
|
||||||
final proposedRoute = await deepLinkHandler.handleMyImmichApp(
|
final proposedRoute = await deepLinkHandler.handleMyImmichApp(
|
||||||
deepLink,
|
deepLink,
|
||||||
currentRouteName == SplashScreenRoute.name,
|
isColdStart,
|
||||||
);
|
);
|
||||||
|
|
||||||
return proposedRoute;
|
return proposedRoute;
|
||||||
@@ -250,7 +253,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
),
|
),
|
||||||
routerConfig: router.config(
|
routerConfig: router.config(
|
||||||
deepLinkBuilder: _deepLinkBuilder,
|
deepLinkBuilder: _deepLinkBuilder,
|
||||||
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
navigatorObservers: () =>
|
||||||
|
[AppNavigationObserver(ref: ref), HeroController()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class TabControllerPage extends HookConsumerWidget {
|
class TabControllerPage extends HookConsumerWidget {
|
||||||
@@ -158,10 +158,6 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final tabsRouter = AutoTabsRouter.of(context);
|
final tabsRouter = AutoTabsRouter.of(context);
|
||||||
final heroedChild = HeroControllerScope(
|
|
||||||
controller: HeroController(),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: tabsRouter.activeIndex == 0,
|
canPop: tabsRouter.activeIndex == 0,
|
||||||
onPopInvokedWithResult: (didPop, _) =>
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
@@ -173,10 +169,10 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
navigationRail(tabsRouter),
|
navigationRail(tabsRouter),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(child: heroedChild),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: heroedChild,
|
: child,
|
||||||
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
||||||
? null
|
? null
|
||||||
: bottomNavigationBar(tabsRouter),
|
: bottomNavigationBar(tabsRouter),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
@@ -127,10 +128,6 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final tabsRouter = AutoTabsRouter.of(context);
|
final tabsRouter = AutoTabsRouter.of(context);
|
||||||
final heroedChild = HeroControllerScope(
|
|
||||||
controller: HeroController(),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: tabsRouter.activeIndex == 0,
|
canPop: tabsRouter.activeIndex == 0,
|
||||||
onPopInvokedWithResult: (didPop, _) =>
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
@@ -142,10 +139,10 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
navigationRail(tabsRouter),
|
navigationRail(tabsRouter),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(child: heroedChild),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: heroedChild,
|
: child,
|
||||||
bottomNavigationBar: _BottomNavigationBar(
|
bottomNavigationBar: _BottomNavigationBar(
|
||||||
tabsRouter: tabsRouter,
|
tabsRouter: tabsRouter,
|
||||||
destinations: navigationDestinations,
|
destinations: navigationDestinations,
|
||||||
@@ -168,6 +165,11 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
|||||||
ref.read(searchInputFocusProvider).requestFocus();
|
ref.read(searchInputFocusProvider).requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Album page
|
||||||
|
if (index == 2) {
|
||||||
|
ref.read(remoteAlbumProvider.notifier).getAll();
|
||||||
|
}
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
router.setActiveIndex(index);
|
router.setActiveIndex(index);
|
||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class DriftPartnerDetailPage extends StatelessWidget {
|
|
||||||
final String partnerId;
|
|
||||||
|
|
||||||
const DriftPartnerDetailPage({super.key, required this.partnerId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ProviderScope(
|
|
||||||
overrides: [
|
|
||||||
timelineServiceProvider.overrideWith(
|
|
||||||
(ref) {
|
|
||||||
final timelineService =
|
|
||||||
ref.watch(timelineFactoryProvider).remoteAssets(partnerId);
|
|
||||||
ref.onDispose(timelineService.dispose);
|
|
||||||
return timelineService;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: const Timeline(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,21 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
final _features = [
|
final _features = [
|
||||||
|
_Feature(
|
||||||
|
name: 'Main Timeline',
|
||||||
|
icon: Icons.timeline_rounded,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Video',
|
||||||
|
icon: Icons.video_collection_outlined,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Recently Taken',
|
||||||
|
icon: Icons.schedule_outlined,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()),
|
||||||
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Selection Mode Timeline',
|
name: 'Selection Mode Timeline',
|
||||||
icon: Icons.developer_mode_rounded,
|
icon: Icons.developer_mode_rounded,
|
||||||
@@ -42,23 +57,28 @@ final _features = [
|
|||||||
return Future.value();
|
return Future.value();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_Feature(
|
||||||
|
name: '',
|
||||||
|
icon: Icons.vertical_align_center_sharp,
|
||||||
|
onTap: (_, __) => Future.value(),
|
||||||
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Sync Local',
|
name: 'Sync Local',
|
||||||
icon: Icons.photo_album_rounded,
|
icon: Icons.photo_album_rounded,
|
||||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Sync Local Full',
|
name: 'Sync Local Full (1)',
|
||||||
icon: Icons.photo_library_rounded,
|
icon: Icons.photo_library_rounded,
|
||||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Hash Local Assets',
|
name: 'Hash Local Assets (2)',
|
||||||
icon: Icons.numbers_outlined,
|
icon: Icons.numbers_outlined,
|
||||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(),
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(),
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Sync Remote',
|
name: 'Sync Remote (3)',
|
||||||
icon: Icons.refresh_rounded,
|
icon: Icons.refresh_rounded,
|
||||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
|
||||||
),
|
),
|
||||||
@@ -69,6 +89,11 @@ final _features = [
|
|||||||
.read(driftProvider)
|
.read(driftProvider)
|
||||||
.customStatement("pragma wal_checkpoint(truncate)"),
|
.customStatement("pragma wal_checkpoint(truncate)"),
|
||||||
),
|
),
|
||||||
|
_Feature(
|
||||||
|
name: '',
|
||||||
|
icon: Icons.vertical_align_center_sharp,
|
||||||
|
onTap: (_, __) => Future.value(),
|
||||||
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Clear Delta Checkpoint',
|
name: 'Clear Delta Checkpoint',
|
||||||
icon: Icons.delete_rounded,
|
icon: Icons.delete_rounded,
|
||||||
@@ -122,21 +147,6 @@ final _features = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_Feature(
|
|
||||||
name: 'Main Timeline',
|
|
||||||
icon: Icons.timeline_rounded,
|
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
|
||||||
),
|
|
||||||
_Feature(
|
|
||||||
name: 'Video',
|
|
||||||
icon: Icons.video_collection_outlined,
|
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
|
|
||||||
),
|
|
||||||
_Feature(
|
|
||||||
name: 'Recently Taken',
|
|
||||||
icon: Icons.schedule_outlined,
|
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class LocalTimelinePage extends StatelessWidget {
|
|
||||||
final String albumId;
|
|
||||||
|
|
||||||
const LocalTimelinePage({super.key, required this.albumId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ProviderScope(
|
|
||||||
overrides: [
|
|
||||||
timelineServiceProvider.overrideWith(
|
|
||||||
(ref) {
|
|
||||||
final timelineService =
|
|
||||||
ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId);
|
|
||||||
ref.onDispose(timelineService.dispose);
|
|
||||||
return timelineService;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: const Timeline(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,17 +16,18 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
return memoryLaneProvider.when(
|
return memoryLaneProvider.when(
|
||||||
data: (memories) {
|
data: (memories) {
|
||||||
return memories.isEmpty
|
return memories.isEmpty
|
||||||
? const Timeline()
|
? const Timeline(showStorageIndicator: true)
|
||||||
: Timeline(
|
: Timeline(
|
||||||
topSliverWidget: SliverToBoxAdapter(
|
topSliverWidget: SliverToBoxAdapter(
|
||||||
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
||||||
child: DriftMemoryLane(memories: memories),
|
child: DriftMemoryLane(memories: memories),
|
||||||
),
|
),
|
||||||
topSliverWidgetHeight: 200,
|
topSliverWidgetHeight: 200,
|
||||||
|
showStorageIndicator: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Timeline(),
|
loading: () => const Timeline(showStorageIndicator: true),
|
||||||
error: (error, stackTrace) => const Timeline(),
|
error: (error, stackTrace) => const Timeline(showStorageIndicator: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
|
|||||||
name: album.name,
|
name: album.name,
|
||||||
countFuture: countFuture,
|
countFuture: countFuture,
|
||||||
onTap: () => context.router.push(
|
onTap: () => context.router.push(
|
||||||
LocalTimelineRoute(albumId: album.id),
|
LocalTimelineRoute(album: album),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -226,7 +226,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
|
|||||||
name: album.name,
|
name: album.name,
|
||||||
countFuture: countFuture,
|
countFuture: countFuture,
|
||||||
onTap: () => context.router.push(
|
onTap: () => context.router.push(
|
||||||
RemoteTimelineRoute(albumId: album.id),
|
RemoteAlbumRoute(album: album),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,7 +97,20 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const ImmichSliverAppBar(),
|
ImmichSliverAppBar(
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
onPressed: () => context.pushRoute(
|
||||||
|
const DriftCreateAlbumRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
showUploadButton: false,
|
||||||
|
),
|
||||||
_SearchBar(
|
_SearchBar(
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
@@ -475,7 +488,7 @@ class _AlbumList extends StatelessWidget {
|
|||||||
|
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String? error;
|
final String? error;
|
||||||
final List<Album> albums;
|
final List<RemoteAlbum> albums;
|
||||||
final String? userId;
|
final String? userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -555,7 +568,7 @@ class _AlbumList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => context.router.push(
|
onTap: () => context.router.push(
|
||||||
RemoteTimelineRoute(albumId: album.id),
|
RemoteAlbumRoute(album: album),
|
||||||
),
|
),
|
||||||
leadingPadding: const EdgeInsets.only(
|
leadingPadding: const EdgeInsets.only(
|
||||||
right: 16,
|
right: 16,
|
||||||
@@ -573,13 +586,24 @@ class _AlbumList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox(
|
: SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
child: Icon(
|
child: Container(
|
||||||
Icons.photo_album_rounded,
|
decoration: BoxDecoration(
|
||||||
size: 40,
|
color: context.colorScheme.surfaceContainer,
|
||||||
color: Colors.grey,
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.outline.withAlpha(50),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.photo_album_rounded,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -599,7 +623,7 @@ class _AlbumGrid extends StatelessWidget {
|
|||||||
required this.error,
|
required this.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<Album> albums;
|
final List<RemoteAlbum> albums;
|
||||||
final String? userId;
|
final String? userId;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String? error;
|
final String? error;
|
||||||
@@ -674,14 +698,14 @@ class _GridAlbumCard extends StatelessWidget {
|
|||||||
required this.userId,
|
required this.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Album album;
|
final RemoteAlbum album;
|
||||||
final String? userId;
|
final String? userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => context.router.push(
|
onTap: () => context.router.push(
|
||||||
RemoteTimelineRoute(albumId: album.id),
|
RemoteAlbumRoute(album: album),
|
||||||
),
|
),
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|||||||
+11
-2
@@ -1,9 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftArchivePage extends StatelessWidget {
|
class DriftArchivePage extends StatelessWidget {
|
||||||
@@ -27,7 +30,13 @@ class DriftArchivePage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(
|
||||||
|
title: 'archive'.t(context: context),
|
||||||
|
icon: Icons.archive_outlined,
|
||||||
|
),
|
||||||
|
bottomSheet: const ArchiveBottomSheet(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget {
|
|||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'User must be logged in to access recently taken',
|
'User must be logged in to access asset selection timeline',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,500 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftCreateAlbumPage extends ConsumerStatefulWidget {
|
||||||
|
const DriftCreateAlbumPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftCreateAlbumPage> createState() =>
|
||||||
|
_DriftCreateAlbumPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
||||||
|
TextEditingController albumTitleController = TextEditingController();
|
||||||
|
TextEditingController albumDescriptionController = TextEditingController();
|
||||||
|
FocusNode albumTitleTextFieldFocusNode = FocusNode();
|
||||||
|
FocusNode albumDescriptionTextFieldFocusNode = FocusNode();
|
||||||
|
bool isAlbumTitleTextFieldFocus = false;
|
||||||
|
Set<BaseAsset> selectedAssets = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
albumTitleController.dispose();
|
||||||
|
albumDescriptionController.dispose();
|
||||||
|
albumTitleTextFieldFocusNode.dispose();
|
||||||
|
albumDescriptionTextFieldFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _canCreateAlbum => albumTitleController.text.isNotEmpty;
|
||||||
|
|
||||||
|
String _getEffectiveTitle() {
|
||||||
|
return albumTitleController.text.isNotEmpty
|
||||||
|
? albumTitleController.text
|
||||||
|
: 'create_album_page_untitled'.t(context: context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSliverAppBar() {
|
||||||
|
return SliverAppBar(
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
elevation: 0,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
pinned: true,
|
||||||
|
snap: false,
|
||||||
|
floating: false,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(200.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildTitleInputField(),
|
||||||
|
buildDescriptionInputField(),
|
||||||
|
if (selectedAssets.isNotEmpty) buildControlButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
if (selectedAssets.isEmpty) {
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
_buildEmptyState(),
|
||||||
|
_buildSelectPhotosButton(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildSelectedImageGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 0, left: 18),
|
||||||
|
child: Text(
|
||||||
|
'create_shared_album_page_share_add_assets',
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
).t(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSelectPhotosButton() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 24.0,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
onPressed: onSelectPhotos,
|
||||||
|
icon: Icon(Icons.add_rounded, color: context.primaryColor),
|
||||||
|
label: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 8.0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'create_shared_album_page_share_select_photos',
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).t(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSelectedImageGrid() {
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
crossAxisSpacing: 1.0,
|
||||||
|
mainAxisSpacing: 1.0,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final asset = selectedAssets.elementAt(index);
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onBackgroundTapped,
|
||||||
|
child: Thumbnail(asset: asset),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: selectedAssets.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBackgroundTapped() {
|
||||||
|
albumTitleTextFieldFocusNode.unfocus();
|
||||||
|
albumDescriptionTextFieldFocusNode.unfocus();
|
||||||
|
setState(() {
|
||||||
|
isAlbumTitleTextFieldFocus = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (albumTitleController.text.isEmpty) {
|
||||||
|
final untitledText = 'create_album_page_untitled'.t();
|
||||||
|
albumTitleController.text = untitledText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onSelectPhotos() async {
|
||||||
|
final assets = await context.pushRoute<Set<BaseAsset>>(
|
||||||
|
DriftAssetSelectionTimelineRoute(
|
||||||
|
lockedSelectionAssets: selectedAssets,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assets == null || assets.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
selectedAssets = selectedAssets.union(assets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createAlbum() async {
|
||||||
|
onBackgroundTapped();
|
||||||
|
|
||||||
|
final title = _getEffectiveTitle().trim();
|
||||||
|
if (title.isEmpty) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('create_album_title_required'.t()),
|
||||||
|
backgroundColor: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final album = await ref.watch(remoteAlbumProvider.notifier).createAlbum(
|
||||||
|
title: title,
|
||||||
|
description: albumDescriptionController.text.trim(),
|
||||||
|
assetIds: selectedAssets.map((asset) {
|
||||||
|
final remoteAsset = asset as RemoteAsset;
|
||||||
|
return remoteAsset.id;
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (album != null) {
|
||||||
|
context.replaceRoute(
|
||||||
|
RemoteAlbumRoute(album: album),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTitleInputField() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 10.0,
|
||||||
|
left: 10.0,
|
||||||
|
),
|
||||||
|
child: _AlbumTitleTextField(
|
||||||
|
focusNode: albumTitleTextFieldFocusNode,
|
||||||
|
textController: albumTitleController,
|
||||||
|
isFocus: isAlbumTitleTextFieldFocus,
|
||||||
|
onFocusChanged: (focus) {
|
||||||
|
setState(() {
|
||||||
|
isAlbumTitleTextFieldFocus = focus;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildDescriptionInputField() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 10.0,
|
||||||
|
left: 10.0,
|
||||||
|
top: 8,
|
||||||
|
),
|
||||||
|
child: _AlbumViewerEditableDescription(
|
||||||
|
textController: albumDescriptionController,
|
||||||
|
focusNode: albumDescriptionTextFieldFocusNode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildControlButton() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 12.0,
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 8.0,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 42.0,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
AlbumActionFilledButton(
|
||||||
|
iconData: Icons.add_photo_alternate_outlined,
|
||||||
|
onPressed: onSelectPhotos,
|
||||||
|
labelText: "add_photos".t(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => context.maybePop(),
|
||||||
|
icon: const Icon(Icons.close_rounded),
|
||||||
|
),
|
||||||
|
title: const Text('create_album').t(),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _canCreateAlbum ? createAlbum : null,
|
||||||
|
child: Text(
|
||||||
|
'create'.t(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _canCreateAlbum
|
||||||
|
? context.primaryColor
|
||||||
|
: context.themeData.disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: GestureDetector(
|
||||||
|
onTap: onBackgroundTapped,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
_buildSliverAppBar(),
|
||||||
|
_buildContent(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumTitleTextField extends StatefulWidget {
|
||||||
|
const _AlbumTitleTextField({
|
||||||
|
required this.focusNode,
|
||||||
|
required this.textController,
|
||||||
|
required this.isFocus,
|
||||||
|
required this.onFocusChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FocusNode focusNode;
|
||||||
|
final TextEditingController textController;
|
||||||
|
final bool isFocus;
|
||||||
|
final ValueChanged<bool> onFocusChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AlbumTitleTextField> createState() => _AlbumTitleTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.focusNode.addListener(_onFocusChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.focusNode.removeListener(_onFocusChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChange() {
|
||||||
|
widget.onFocusChanged(widget.focusNode.hasFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28.0,
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
controller: widget.textController,
|
||||||
|
onTap: () {
|
||||||
|
if (widget.textController.text ==
|
||||||
|
'create_album_page_untitled'.t(context: context)) {
|
||||||
|
widget.textController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
suffixIcon: widget.textController.text.isNotEmpty && widget.isFocus
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.textController.clear();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
splashRadius: 10.0,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
enabledBorder: const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(16.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.primaryColor.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hintText: 'add_a_title'.t(),
|
||||||
|
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
|
||||||
|
fontSize: 28.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
focusColor: Colors.grey[300],
|
||||||
|
fillColor: context.colorScheme.surfaceContainerHigh,
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumViewerEditableDescription extends StatefulWidget {
|
||||||
|
const _AlbumViewerEditableDescription({
|
||||||
|
required this.textController,
|
||||||
|
required this.focusNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController textController;
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AlbumViewerEditableDescription> createState() =>
|
||||||
|
_AlbumViewerEditableDescriptionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlbumViewerEditableDescriptionState
|
||||||
|
extends State<_AlbumViewerEditableDescription> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.focusNode.addListener(_onFocusModeChange);
|
||||||
|
widget.textController.addListener(_onTextChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.focusNode.removeListener(_onFocusModeChange);
|
||||||
|
widget.textController.removeListener(_onTextChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusModeChange() {
|
||||||
|
setState(() {
|
||||||
|
if (!widget.focusNode.hasFocus && widget.textController.text.isEmpty) {
|
||||||
|
widget.textController.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTextChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
maxLines: 3,
|
||||||
|
minLines: 1,
|
||||||
|
controller: widget.textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
suffixIcon:
|
||||||
|
widget.focusNode.hasFocus && widget.textController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.textController.clear();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
splashRadius: 10.0,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.primaryColor.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(16.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
|
||||||
|
fontSize: 16.0,
|
||||||
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
focusColor: Colors.grey[300],
|
||||||
|
fillColor: context.scaffoldBackgroundColor,
|
||||||
|
filled: widget.focusNode.hasFocus,
|
||||||
|
hintText: 'add_a_description'.t(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-2
@@ -1,9 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftFavoritePage extends StatelessWidget {
|
class DriftFavoritePage extends StatelessWidget {
|
||||||
@@ -27,7 +30,13 @@ class DriftFavoritePage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(
|
||||||
|
title: 'favorites'.t(context: context),
|
||||||
|
icon: Icons.favorite_outline,
|
||||||
|
),
|
||||||
|
bottomSheet: const FavoriteBottomSheet(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,12 @@ class DriftLibraryPage extends ConsumerWidget {
|
|||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
ImmichSliverAppBar(),
|
ImmichSliverAppBar(
|
||||||
|
snap: false,
|
||||||
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
|
showUploadButton: false,
|
||||||
|
),
|
||||||
_ActionButtonGrid(),
|
_ActionButtonGrid(),
|
||||||
_CollectionCards(),
|
_CollectionCards(),
|
||||||
_QuickAccessButtonList(),
|
_QuickAccessButtonList(),
|
||||||
@@ -507,8 +512,9 @@ class _PartnerList extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
).t(context: context, args: {'user': partner.name}),
|
).t(context: context, args: {'user': partner.name}),
|
||||||
onTap: () =>
|
onTap: () => context.pushRoute(
|
||||||
context.pushRoute(DriftPartnerDetailRoute(partnerId: partner.id)),
|
DriftPartnerDetailRoute(partner: partner),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -103,7 +103,7 @@ class _AlbumList extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
context.pushRoute(LocalTimelineRoute(albumId: album.id)),
|
context.pushRoute(LocalTimelineRoute(album: album)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
+9
-1
@@ -1,9 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftLockedFolderPage extends StatelessWidget {
|
class DriftLockedFolderPage extends StatelessWidget {
|
||||||
@@ -27,7 +30,12 @@ class DriftLockedFolderPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(
|
||||||
|
title: 'locked_folder'.t(context: context),
|
||||||
|
),
|
||||||
|
bottomSheet: const LockedFolderBottomSheet(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftPartnerDetailPage extends StatelessWidget {
|
||||||
|
final UserDto partner;
|
||||||
|
|
||||||
|
const DriftPartnerDetailPage({
|
||||||
|
super.key,
|
||||||
|
required this.partner,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).remoteAssets(partner.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(
|
||||||
|
title: partner.name,
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
),
|
||||||
|
topSliverWidget: _InfoBox(
|
||||||
|
onTap: () => {
|
||||||
|
// TODO: Create DriftUserProvider/DriftUserService to handle this action
|
||||||
|
},
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
),
|
||||||
|
topSliverWidgetHeight: 110,
|
||||||
|
bottomSheet: const PartnerDetailBottomSheet(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InfoBox extends StatelessWidget {
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool inTimeline;
|
||||||
|
|
||||||
|
const _InfoBox({
|
||||||
|
required this.onTap,
|
||||||
|
required this.inTimeline,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 110,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
context.colorScheme.primary.withAlpha(15),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
"Show in timeline",
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"Show photos and videos from this user in your timeline",
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: inTimeline,
|
||||||
|
onChanged: (_) => onTap(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class RemoteAlbumPage extends StatelessWidget {
|
||||||
|
final RemoteAlbum album;
|
||||||
|
|
||||||
|
const RemoteAlbumPage({super.key, required this.album});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final timelineService = ref
|
||||||
|
.watch(timelineFactoryProvider)
|
||||||
|
.remoteAlbum(albumId: album.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(
|
||||||
|
title: album.name,
|
||||||
|
icon: Icons.photo_album_outlined,
|
||||||
|
),
|
||||||
|
bottomSheet: RemoteAlbumBottomSheet(
|
||||||
|
album: album,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-2
@@ -1,6 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@@ -27,7 +28,16 @@ class DriftTrashPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: SliverAppBar(
|
||||||
|
title: Text('trash'.t(context: context)),
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
pinned: true,
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-5
@@ -1,14 +1,17 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class RemoteTimelinePage extends StatelessWidget {
|
class LocalTimelinePage extends StatelessWidget {
|
||||||
final String albumId;
|
final LocalAlbum album;
|
||||||
|
|
||||||
const RemoteTimelinePage({super.key, required this.albumId});
|
const LocalTimelinePage({super.key, required this.album});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -18,13 +21,16 @@ class RemoteTimelinePage extends StatelessWidget {
|
|||||||
(ref) {
|
(ref) {
|
||||||
final timelineService = ref
|
final timelineService = ref
|
||||||
.watch(timelineFactoryProvider)
|
.watch(timelineFactoryProvider)
|
||||||
.remoteAlbum(albumId: albumId);
|
.localAlbum(albumId: album.id);
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const Timeline(),
|
child: Timeline(
|
||||||
|
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||||
|
bottomSheet: const LocalAlbumBottomSheet(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+41
-1
@@ -1,16 +1,56 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class RemoveFromAlbumActionButton extends ConsumerWidget {
|
class RemoveFromAlbumActionButton extends ConsumerWidget {
|
||||||
const RemoveFromAlbumActionButton({super.key});
|
final String albumId;
|
||||||
|
final ActionSource source;
|
||||||
|
|
||||||
|
const RemoveFromAlbumActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.albumId,
|
||||||
|
required this.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref
|
||||||
|
.read(actionProvider.notifier)
|
||||||
|
.removeFromAlbum(source, albumId);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'remove_from_album_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.remove_circle_outline,
|
iconData: Icons.remove_circle_outline,
|
||||||
label: "remove_from_album".t(context: context),
|
label: "remove_from_album".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class UnarchiveActionButton extends ConsumerWidget {
|
class UnArchiveActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
|
||||||
const UnarchiveActionButton({super.key, required this.source});
|
const UnArchiveActionButton({super.key, required this.source});
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
StreamSubscription? reloadSubscription;
|
StreamSubscription? reloadSubscription;
|
||||||
|
|
||||||
late Platform platform;
|
late Platform platform;
|
||||||
|
late final int heroOffset;
|
||||||
late PhotoViewControllerValue initialPhotoViewState;
|
late PhotoViewControllerValue initialPhotoViewState;
|
||||||
bool? hasDraggedDown;
|
bool? hasDraggedDown;
|
||||||
bool isSnapping = false;
|
bool isSnapping = false;
|
||||||
@@ -98,6 +99,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
_onAssetChanged(widget.initialIndex);
|
_onAssetChanged(widget.initialIndex);
|
||||||
});
|
});
|
||||||
reloadSubscription = EventStream.shared.listen(_onEvent);
|
reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -335,7 +337,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
final isDraggingDown = currentExtent < previousExtent;
|
final isDraggingDown = currentExtent < previousExtent;
|
||||||
previousExtent = currentExtent;
|
previousExtent = currentExtent;
|
||||||
// Closes the bottom sheet if the user is dragging down
|
// Closes the bottom sheet if the user is dragging down
|
||||||
if (isDraggingDown && delta.extent < 0.5) {
|
if (isDraggingDown && delta.extent < 0.55) {
|
||||||
if (dragInProgress) {
|
if (dragInProgress) {
|
||||||
blockGestures = true;
|
blockGestures = true;
|
||||||
}
|
}
|
||||||
@@ -400,7 +402,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
previousExtent = _kBottomSheetMinimumExtent;
|
previousExtent = _kBottomSheetMinimumExtent;
|
||||||
sheetCloseController = showBottomSheet(
|
sheetCloseController = showBottomSheet(
|
||||||
context: ctx,
|
context: ctx,
|
||||||
sheetAnimationStyle: AnimationStyle(
|
sheetAnimationStyle: const AnimationStyle(
|
||||||
duration: Durations.short4,
|
duration: Durations.short4,
|
||||||
reverseDuration: Durations.short2,
|
reverseDuration: Durations.short2,
|
||||||
),
|
),
|
||||||
@@ -487,7 +489,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
key: ValueKey(asset.heroTag),
|
key: ValueKey(asset.heroTag),
|
||||||
imageProvider: getFullImageProvider(asset, size: size),
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
heroAttributes:
|
||||||
|
PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
tightMode: true,
|
tightMode: true,
|
||||||
initialScale: PhotoViewComputedScale.contained * 0.999,
|
initialScale: PhotoViewComputedScale.contained * 0.999,
|
||||||
@@ -521,7 +524,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
onTapDown: _onTapDown,
|
onTapDown: _onTapDown,
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
heroAttributes:
|
||||||
|
PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
initialScale: PhotoViewComputedScale.contained * 0.99,
|
initialScale: PhotoViewComputedScale.contained * 0.99,
|
||||||
maxScale: 1.0,
|
maxScale: 1.0,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
|
class ArchiveBottomSheet extends ConsumerWidget {
|
||||||
|
const ArchiveBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
|
final isTrashEnable = ref.watch(
|
||||||
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
|
);
|
||||||
|
|
||||||
|
return BaseBottomSheet(
|
||||||
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
actions: [
|
||||||
|
const ShareActionButton(),
|
||||||
|
if (multiselect.hasRemote) ...[
|
||||||
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
|
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||||
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
|
const DownloadActionButton(),
|
||||||
|
isTrashEnable
|
||||||
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
|
: const DeletePermanentActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
|
const EditDateTimeActionButton(),
|
||||||
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
|
const MoveToLockFolderActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
|
const StackActionButton(),
|
||||||
|
],
|
||||||
|
if (multiselect.hasLocal) ...[
|
||||||
|
const DeleteLocalActionButton(),
|
||||||
|
const UploadActionButton(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
|
class FavoriteBottomSheet extends ConsumerWidget {
|
||||||
|
const FavoriteBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
|
final isTrashEnable = ref.watch(
|
||||||
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
|
);
|
||||||
|
|
||||||
|
return BaseBottomSheet(
|
||||||
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
actions: [
|
||||||
|
const ShareActionButton(),
|
||||||
|
if (multiselect.hasRemote) ...[
|
||||||
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
|
const UnFavoriteActionButton(source: ActionSource.timeline),
|
||||||
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
|
const DownloadActionButton(),
|
||||||
|
isTrashEnable
|
||||||
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
|
: const DeletePermanentActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
|
const EditDateTimeActionButton(),
|
||||||
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
|
const MoveToLockFolderActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
|
const StackActionButton(),
|
||||||
|
],
|
||||||
|
if (multiselect.hasLocal) ...[
|
||||||
|
const DeleteLocalActionButton(),
|
||||||
|
const UploadActionButton(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -14,12 +14,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class HomeBottomAppBar extends ConsumerWidget {
|
class GeneralBottomSheet extends ConsumerWidget {
|
||||||
const HomeBottomAppBar({super.key});
|
const GeneralBottomSheet({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -30,9 +30,10 @@ class HomeBottomAppBar extends ConsumerWidget {
|
|||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
initialChildSize: 0.25,
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (multiselect.isEnabled) const ShareActionButton(),
|
const ShareActionButton(),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
|
||||||
|
class LocalAlbumBottomSheet extends ConsumerWidget {
|
||||||
|
const LocalAlbumBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const BaseBottomSheet(
|
||||||
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
actions: [
|
||||||
|
ShareActionButton(),
|
||||||
|
DeleteLocalActionButton(),
|
||||||
|
UploadActionButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
|
||||||
|
class LockedFolderBottomSheet extends ConsumerWidget {
|
||||||
|
const LockedFolderBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const BaseBottomSheet(
|
||||||
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
actions: [
|
||||||
|
ShareActionButton(),
|
||||||
|
DownloadActionButton(),
|
||||||
|
DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
|
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
|
||||||
|
class PartnerDetailBottomSheet extends ConsumerWidget {
|
||||||
|
const PartnerDetailBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const BaseBottomSheet(
|
||||||
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
actions: [
|
||||||
|
ShareActionButton(),
|
||||||
|
DownloadActionButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||||
|
final RemoteAlbum album;
|
||||||
|
const RemoteAlbumBottomSheet({super.key, required this.album});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
|
final isTrashEnable = ref.watch(
|
||||||
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
|
);
|
||||||
|
|
||||||
|
return BaseBottomSheet(
|
||||||
|
initialChildSize: 0.25,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
actions: [
|
||||||
|
const ShareActionButton(),
|
||||||
|
if (multiselect.hasRemote) ...[
|
||||||
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
|
const DownloadActionButton(),
|
||||||
|
isTrashEnable
|
||||||
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
|
: const DeletePermanentActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
|
const EditDateTimeActionButton(),
|
||||||
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
|
const MoveToLockFolderActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
|
const StackActionButton(),
|
||||||
|
],
|
||||||
|
if (multiselect.hasLocal) ...[
|
||||||
|
const DeleteLocalActionButton(),
|
||||||
|
const UploadActionButton(),
|
||||||
|
],
|
||||||
|
RemoveFromAlbumActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
albumId: album.id,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -25,6 +26,8 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||||
|
|
||||||
final assetContainerColor = context.isDarkTheme
|
final assetContainerColor = context.isDarkTheme
|
||||||
? context.primaryColor.darken(amount: 0.4)
|
? context.primaryColor.darken(amount: 0.4)
|
||||||
: context.primaryColor.lighten(amount: 0.75);
|
: context.primaryColor.lighten(amount: 0.75);
|
||||||
@@ -64,7 +67,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: asset.heroTag,
|
tag: '${asset.heroTag}_$heroOffset',
|
||||||
child: Thumbnail(
|
child: Thumbnail(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
|||||||
@@ -159,17 +159,18 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
required this.assetIndex,
|
required this.assetIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
void _handleOnTap(
|
Future _handleOnTap(
|
||||||
BuildContext ctx,
|
BuildContext ctx,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
int assetIndex,
|
int assetIndex,
|
||||||
BaseAsset asset,
|
BaseAsset asset,
|
||||||
) {
|
) async {
|
||||||
final multiSelectState = ref.read(multiSelectProvider);
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
|
||||||
if (multiSelectState.forceEnable || multiSelectState.isEnabled) {
|
if (multiSelectState.forceEnable || multiSelectState.isEnabled) {
|
||||||
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||||
} else {
|
} else {
|
||||||
|
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
|
||||||
ctx.pushRoute(
|
ctx.pushRoute(
|
||||||
AssetViewerRoute(
|
AssetViewerRoute(
|
||||||
initialIndex: assetIndex,
|
initialIndex: assetIndex,
|
||||||
@@ -206,6 +207,9 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final lockSelection = _getLockSelectionStatus(ref);
|
final lockSelection = _getLockSelectionStatus(ref);
|
||||||
|
final showStorageIndicator = ref.watch(
|
||||||
|
timelineArgsProvider.select((args) => args.showStorageIndicator),
|
||||||
|
);
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -217,6 +221,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
child: ThumbnailTile(
|
child: ThumbnailTile(
|
||||||
asset,
|
asset,
|
||||||
lockSelection: lockSelection,
|
lockSelection: lockSelection,
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ class TimelineArgs {
|
|||||||
final double maxHeight;
|
final double maxHeight;
|
||||||
final double spacing;
|
final double spacing;
|
||||||
final int columnCount;
|
final int columnCount;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
|
||||||
const TimelineArgs({
|
const TimelineArgs({
|
||||||
required this.maxWidth,
|
required this.maxWidth,
|
||||||
required this.maxHeight,
|
required this.maxHeight,
|
||||||
this.spacing = kTimelineSpacing,
|
this.spacing = kTimelineSpacing,
|
||||||
this.columnCount = kTimelineColumnCount,
|
this.columnCount = kTimelineColumnCount,
|
||||||
|
this.showStorageIndicator = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -27,7 +29,8 @@ class TimelineArgs {
|
|||||||
return spacing == other.spacing &&
|
return spacing == other.spacing &&
|
||||||
maxWidth == other.maxWidth &&
|
maxWidth == other.maxWidth &&
|
||||||
maxHeight == other.maxHeight &&
|
maxHeight == other.maxHeight &&
|
||||||
columnCount == other.columnCount;
|
columnCount == other.columnCount &&
|
||||||
|
showStorageIndicator == other.showStorageIndicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,7 +38,8 @@ class TimelineArgs {
|
|||||||
maxWidth.hashCode ^
|
maxWidth.hashCode ^
|
||||||
maxHeight.hashCode ^
|
maxHeight.hashCode ^
|
||||||
spacing.hashCode ^
|
spacing.hashCode ^
|
||||||
columnCount.hashCode;
|
columnCount.hashCode ^
|
||||||
|
showStorageIndicator.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineState {
|
class TimelineState {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
@@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
||||||
|
|
||||||
class Timeline extends StatelessWidget {
|
class Timeline extends StatelessWidget {
|
||||||
@@ -25,11 +26,16 @@ class Timeline extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
|
this.showStorageIndicator = false,
|
||||||
|
this.appBar,
|
||||||
|
this.bottomSheet = const GeneralBottomSheet(),
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
final double? topSliverWidgetHeight;
|
final double? topSliverWidgetHeight;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
final Widget? appBar;
|
||||||
|
final Widget? bottomSheet;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -43,12 +49,15 @@ class Timeline extends StatelessWidget {
|
|||||||
columnCount: ref.watch(
|
columnCount: ref.watch(
|
||||||
settingsProvider.select((s) => s.get(Setting.tilesPerRow)),
|
settingsProvider.select((s) => s.get(Setting.tilesPerRow)),
|
||||||
),
|
),
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: _SliverTimeline(
|
child: _SliverTimeline(
|
||||||
topSliverWidget: topSliverWidget,
|
topSliverWidget: topSliverWidget,
|
||||||
topSliverWidgetHeight: topSliverWidgetHeight,
|
topSliverWidgetHeight: topSliverWidgetHeight,
|
||||||
|
appBar: appBar,
|
||||||
|
bottomSheet: bottomSheet,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -60,10 +69,14 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
const _SliverTimeline({
|
const _SliverTimeline({
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
|
this.appBar,
|
||||||
|
this.bottomSheet,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
final double? topSliverWidgetHeight;
|
final double? topSliverWidgetHeight;
|
||||||
|
final Widget? appBar;
|
||||||
|
final Widget? bottomSheet;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
@@ -100,6 +113,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
onData: (segments) {
|
onData: (segments) {
|
||||||
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
||||||
final statusBarHeight = context.padding.top;
|
final statusBarHeight = context.padding.top;
|
||||||
|
final double appBarExpandedHeight =
|
||||||
|
widget.appBar != null && widget.appBar is MesmerizingSliverAppBar
|
||||||
|
? 200
|
||||||
|
: 0;
|
||||||
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
|
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
|
||||||
const scrubberBottomPadding = 100.0;
|
const scrubberBottomPadding = 100.0;
|
||||||
|
|
||||||
@@ -112,7 +129,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
timelineHeight: maxHeight,
|
timelineHeight: maxHeight,
|
||||||
topPadding: totalAppBarHeight + 10,
|
topPadding: totalAppBarHeight + 10,
|
||||||
bottomPadding: context.padding.bottom + scrubberBottomPadding,
|
bottomPadding: context.padding.bottom + scrubberBottomPadding,
|
||||||
monthSegmentSnappingOffset: widget.topSliverWidgetHeight,
|
monthSegmentSnappingOffset:
|
||||||
|
widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
cacheExtent: maxHeight * 2,
|
cacheExtent: maxHeight * 2,
|
||||||
@@ -120,11 +138,12 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
if (isSelectionMode)
|
if (isSelectionMode)
|
||||||
const SelectionSliverAppBar()
|
const SelectionSliverAppBar()
|
||||||
else
|
else
|
||||||
const ImmichSliverAppBar(
|
widget.appBar ??
|
||||||
floating: true,
|
const ImmichSliverAppBar(
|
||||||
pinned: false,
|
floating: true,
|
||||||
snap: false,
|
pinned: false,
|
||||||
),
|
snap: false,
|
||||||
|
),
|
||||||
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
||||||
_SliverSegmentedList(
|
_SliverSegmentedList(
|
||||||
segments: segments,
|
segments: segments,
|
||||||
@@ -182,7 +201,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
child: const HomeBottomAppBar(),
|
child: widget.bottomSheet,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -228,6 +228,24 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> removeFromAlbum(
|
||||||
|
ActionSource source,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
|
final ids = _getRemoteIdsForSource(source);
|
||||||
|
try {
|
||||||
|
final removedCount = await _service.removeFromAlbum(ids, albumId);
|
||||||
|
return ActionResult(count: removedCount, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to remove assets from album', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Iterable<RemoteAsset> {
|
extension on Iterable<RemoteAsset> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
|
||||||
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
||||||
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
||||||
@@ -30,7 +31,10 @@ final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
|
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
|
||||||
(ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository)),
|
(ref) => RemoteAlbumService(
|
||||||
|
ref.watch(remoteAlbumRepository),
|
||||||
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
|
),
|
||||||
dependencies: [remoteAlbumRepository],
|
dependencies: [remoteAlbumRepository],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'album.provider.dart';
|
import 'album.provider.dart';
|
||||||
|
|
||||||
class RemoteAlbumState {
|
class RemoteAlbumState {
|
||||||
final List<Album> albums;
|
final List<RemoteAlbum> albums;
|
||||||
final List<Album> filteredAlbums;
|
final List<RemoteAlbum> filteredAlbums;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String? error;
|
final String? error;
|
||||||
|
|
||||||
const RemoteAlbumState({
|
const RemoteAlbumState({
|
||||||
required this.albums,
|
required this.albums,
|
||||||
List<Album>? filteredAlbums,
|
List<RemoteAlbum>? filteredAlbums,
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
this.error,
|
this.error,
|
||||||
}) : filteredAlbums = filteredAlbums ?? albums;
|
}) : filteredAlbums = filteredAlbums ?? albums;
|
||||||
|
|
||||||
RemoteAlbumState copyWith({
|
RemoteAlbumState copyWith({
|
||||||
List<Album>? albums,
|
List<RemoteAlbum>? albums,
|
||||||
List<Album>? filteredAlbums,
|
List<RemoteAlbum>? filteredAlbums,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
String? error,
|
String? error,
|
||||||
}) {
|
}) {
|
||||||
@@ -66,7 +66,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Album>> getAll() async {
|
Future<List<RemoteAlbum>> getAll() async {
|
||||||
state = state.copyWith(isLoading: true, error: null);
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -118,4 +118,31 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
||||||
state = state.copyWith(filteredAlbums: sortedAlbums);
|
state = state.copyWith(filteredAlbums: sortedAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum?> createAlbum({
|
||||||
|
required String title,
|
||||||
|
String? description,
|
||||||
|
List<String> assetIds = const [],
|
||||||
|
}) async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final album = await _remoteAlbumService.createAlbum(
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
albums: [...state.albums, album],
|
||||||
|
filteredAlbums: [...state.filteredAlbums, album],
|
||||||
|
);
|
||||||
|
|
||||||
|
state = state.copyWith(isLoading: false);
|
||||||
|
return album;
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart'
|
||||||
|
show AlbumAssetOrder, RemoteAlbum;
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||||
@@ -50,6 +52,25 @@ class AlbumApiRepository extends ApiRepository {
|
|||||||
return _toAlbum(responseDto);
|
return _toAlbum(responseDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Change name after removing old method
|
||||||
|
Future<RemoteAlbum> createDriftAlbum(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
String? description,
|
||||||
|
}) async {
|
||||||
|
final responseDto = await checkNull(
|
||||||
|
_api.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _toRemoteAlbum(responseDto);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Album> update(
|
Future<Album> update(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
String? name,
|
String? name,
|
||||||
@@ -170,4 +191,22 @@ class AlbumApiRepository extends ApiRepository {
|
|||||||
|
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) {
|
||||||
|
return RemoteAlbum(
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.albumName,
|
||||||
|
ownerId: dto.owner.id,
|
||||||
|
description: dto.description,
|
||||||
|
createdAt: dto.createdAt,
|
||||||
|
updatedAt: dto.updatedAt,
|
||||||
|
thumbnailAssetId: dto.albumThumbnailAssetId,
|
||||||
|
isActivityEnabled: dto.isActivityEnabled,
|
||||||
|
order: dto.order == AssetOrder.asc
|
||||||
|
? AlbumAssetOrder.asc
|
||||||
|
: AlbumAssetOrder.desc,
|
||||||
|
assetCount: dto.assetCount,
|
||||||
|
ownerName: dto.owner.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||||
|
// ignore: import_rule_openapi
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final driftAlbumApiRepositoryProvider = Provider(
|
||||||
|
(ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||||
|
);
|
||||||
|
|
||||||
|
class DriftAlbumApiRepository extends ApiRepository {
|
||||||
|
final AlbumsApi _api;
|
||||||
|
|
||||||
|
DriftAlbumApiRepository(this._api);
|
||||||
|
|
||||||
|
Future<RemoteAlbum> createDriftAlbum(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
String? description,
|
||||||
|
}) async {
|
||||||
|
final responseDto = await checkNull(
|
||||||
|
_api.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return responseDto.toRemoteAlbum();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
) async {
|
||||||
|
final response = await checkNull(
|
||||||
|
_api.removeAssetFromAlbum(
|
||||||
|
albumId,
|
||||||
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final List<String> removed = [], failed = [];
|
||||||
|
for (final dto in response) {
|
||||||
|
if (dto.success) {
|
||||||
|
removed.add(dto.id);
|
||||||
|
} else {
|
||||||
|
failed.add(dto.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (removed: removed, failed: failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on AlbumResponseDto {
|
||||||
|
RemoteAlbum toRemoteAlbum() {
|
||||||
|
return RemoteAlbum(
|
||||||
|
id: id,
|
||||||
|
name: albumName,
|
||||||
|
ownerId: owner.id,
|
||||||
|
description: description,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
thumbnailAssetId: albumThumbnailAssetId,
|
||||||
|
isActivityEnabled: isActivityEnabled,
|
||||||
|
order:
|
||||||
|
order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
|
||||||
|
assetCount: assetCount,
|
||||||
|
ownerName: owner.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
@@ -67,22 +69,23 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_partner_detail.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_local_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_recently_taken.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_locked_folder.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@@ -387,7 +390,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: RemoteTimelineRoute.page,
|
page: RemoteAlbumRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
@@ -446,6 +449,11 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: DriftLocalAlbumsRoute.page,
|
page: DriftLocalAlbumsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftCreateAlbumRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
|||||||
@@ -683,6 +683,22 @@ class DriftAssetSelectionTimelineRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftCreateAlbumPage]
|
||||||
|
class DriftCreateAlbumRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftCreateAlbumRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftCreateAlbumRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftCreateAlbumRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftCreateAlbumPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftFavoritePage]
|
/// [DriftFavoritePage]
|
||||||
class DriftFavoriteRoute extends PageRouteInfo<void> {
|
class DriftFavoriteRoute extends PageRouteInfo<void> {
|
||||||
@@ -805,11 +821,11 @@ class DriftPartnerDetailRoute
|
|||||||
extends PageRouteInfo<DriftPartnerDetailRouteArgs> {
|
extends PageRouteInfo<DriftPartnerDetailRouteArgs> {
|
||||||
DriftPartnerDetailRoute({
|
DriftPartnerDetailRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required String partnerId,
|
required UserDto partner,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
DriftPartnerDetailRoute.name,
|
DriftPartnerDetailRoute.name,
|
||||||
args: DriftPartnerDetailRouteArgs(key: key, partnerId: partnerId),
|
args: DriftPartnerDetailRouteArgs(key: key, partner: partner),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -819,21 +835,21 @@ class DriftPartnerDetailRoute
|
|||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final args = data.argsAs<DriftPartnerDetailRouteArgs>();
|
final args = data.argsAs<DriftPartnerDetailRouteArgs>();
|
||||||
return DriftPartnerDetailPage(key: args.key, partnerId: args.partnerId);
|
return DriftPartnerDetailPage(key: args.key, partner: args.partner);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DriftPartnerDetailRouteArgs {
|
class DriftPartnerDetailRouteArgs {
|
||||||
const DriftPartnerDetailRouteArgs({this.key, required this.partnerId});
|
const DriftPartnerDetailRouteArgs({this.key, required this.partner});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final String partnerId;
|
final UserDto partner;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'DriftPartnerDetailRouteArgs{key: $key, partnerId: $partnerId}';
|
return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1211,11 +1227,11 @@ class LocalMediaSummaryRoute extends PageRouteInfo<void> {
|
|||||||
class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
|
class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
|
||||||
LocalTimelineRoute({
|
LocalTimelineRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required String albumId,
|
required LocalAlbum album,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
LocalTimelineRoute.name,
|
LocalTimelineRoute.name,
|
||||||
args: LocalTimelineRouteArgs(key: key, albumId: albumId),
|
args: LocalTimelineRouteArgs(key: key, album: album),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1225,21 +1241,21 @@ class LocalTimelineRoute extends PageRouteInfo<LocalTimelineRouteArgs> {
|
|||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final args = data.argsAs<LocalTimelineRouteArgs>();
|
final args = data.argsAs<LocalTimelineRouteArgs>();
|
||||||
return LocalTimelinePage(key: args.key, albumId: args.albumId);
|
return LocalTimelinePage(key: args.key, album: args.album);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalTimelineRouteArgs {
|
class LocalTimelineRouteArgs {
|
||||||
const LocalTimelineRouteArgs({this.key, required this.albumId});
|
const LocalTimelineRouteArgs({this.key, required this.album});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final String albumId;
|
final LocalAlbum album;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'LocalTimelineRouteArgs{key: $key, albumId: $albumId}';
|
return 'LocalTimelineRouteArgs{key: $key, album: $album}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1744,6 +1760,43 @@ class RecentlyTakenRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [RemoteAlbumPage]
|
||||||
|
class RemoteAlbumRoute extends PageRouteInfo<RemoteAlbumRouteArgs> {
|
||||||
|
RemoteAlbumRoute({
|
||||||
|
Key? key,
|
||||||
|
required RemoteAlbum album,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
RemoteAlbumRoute.name,
|
||||||
|
args: RemoteAlbumRouteArgs(key: key, album: album),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'RemoteAlbumRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<RemoteAlbumRouteArgs>();
|
||||||
|
return RemoteAlbumPage(key: args.key, album: args.album);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteAlbumRouteArgs {
|
||||||
|
const RemoteAlbumRouteArgs({this.key, required this.album});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final RemoteAlbum album;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'RemoteAlbumRouteArgs{key: $key, album: $album}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [RemoteMediaSummaryPage]
|
/// [RemoteMediaSummaryPage]
|
||||||
class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
|
class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
|
||||||
@@ -1760,43 +1813,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [RemoteTimelinePage]
|
|
||||||
class RemoteTimelineRoute extends PageRouteInfo<RemoteTimelineRouteArgs> {
|
|
||||||
RemoteTimelineRoute({
|
|
||||||
Key? key,
|
|
||||||
required String albumId,
|
|
||||||
List<PageRouteInfo>? children,
|
|
||||||
}) : super(
|
|
||||||
RemoteTimelineRoute.name,
|
|
||||||
args: RemoteTimelineRouteArgs(key: key, albumId: albumId),
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'RemoteTimelineRoute';
|
|
||||||
|
|
||||||
static PageInfo page = PageInfo(
|
|
||||||
name,
|
|
||||||
builder: (data) {
|
|
||||||
final args = data.argsAs<RemoteTimelineRouteArgs>();
|
|
||||||
return RemoteTimelinePage(key: args.key, albumId: args.albumId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteTimelineRouteArgs {
|
|
||||||
const RemoteTimelineRouteArgs({this.key, required this.albumId});
|
|
||||||
|
|
||||||
final Key? key;
|
|
||||||
|
|
||||||
final String albumId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'RemoteTimelineRouteArgs{key: $key, albumId: $albumId}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [SearchPage]
|
/// [SearchPage]
|
||||||
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
@@ -14,16 +17,22 @@ final actionServiceProvider = Provider<ActionService>(
|
|||||||
(ref) => ActionService(
|
(ref) => ActionService(
|
||||||
ref.watch(assetApiRepositoryProvider),
|
ref.watch(assetApiRepositoryProvider),
|
||||||
ref.watch(remoteAssetRepositoryProvider),
|
ref.watch(remoteAssetRepositoryProvider),
|
||||||
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
|
ref.watch(remoteAlbumRepository),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class ActionService {
|
class ActionService {
|
||||||
final AssetApiRepository _assetApiRepository;
|
final AssetApiRepository _assetApiRepository;
|
||||||
final RemoteAssetRepository _remoteAssetRepository;
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
|
|
||||||
const ActionService(
|
const ActionService(
|
||||||
this._assetApiRepository,
|
this._assetApiRepository,
|
||||||
this._remoteAssetRepository,
|
this._remoteAssetRepository,
|
||||||
|
this._albumApiRepository,
|
||||||
|
this._remoteAlbumRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||||
@@ -131,4 +140,16 @@ class ActionService {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
||||||
|
int removedCount = 0;
|
||||||
|
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
||||||
|
|
||||||
|
if (result.removed.isNotEmpty) {
|
||||||
|
removedCount =
|
||||||
|
await _remoteAlbumRepository.removeAssets(albumId, result.removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return removedCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,56 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
|
||||||
typedef AlbumSortFn = List<Album> Function(List<Album> albums, bool isReverse);
|
typedef AlbumSortFn = List<RemoteAlbum> Function(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
);
|
||||||
|
|
||||||
class _RemoteAlbumSortHandlers {
|
class _RemoteAlbumSortHandlers {
|
||||||
const _RemoteAlbumSortHandlers._();
|
const _RemoteAlbumSortHandlers._();
|
||||||
|
|
||||||
static const AlbumSortFn created = _sortByCreated;
|
static const AlbumSortFn created = _sortByCreated;
|
||||||
static List<Album> _sortByCreated(List<Album> albums, bool isReverse) {
|
static List<RemoteAlbum> _sortByCreated(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
) {
|
||||||
final sorted = albums.sortedBy((album) => album.createdAt);
|
final sorted = albums.sortedBy((album) => album.createdAt);
|
||||||
return (isReverse ? sorted.reversed : sorted).toList();
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static const AlbumSortFn title = _sortByTitle;
|
static const AlbumSortFn title = _sortByTitle;
|
||||||
static List<Album> _sortByTitle(List<Album> albums, bool isReverse) {
|
static List<RemoteAlbum> _sortByTitle(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
) {
|
||||||
final sorted = albums.sortedBy((album) => album.name);
|
final sorted = albums.sortedBy((album) => album.name);
|
||||||
return (isReverse ? sorted.reversed : sorted).toList();
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static const AlbumSortFn lastModified = _sortByLastModified;
|
static const AlbumSortFn lastModified = _sortByLastModified;
|
||||||
static List<Album> _sortByLastModified(List<Album> albums, bool isReverse) {
|
static List<RemoteAlbum> _sortByLastModified(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
) {
|
||||||
final sorted = albums.sortedBy((album) => album.updatedAt);
|
final sorted = albums.sortedBy((album) => album.updatedAt);
|
||||||
return (isReverse ? sorted.reversed : sorted).toList();
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static const AlbumSortFn assetCount = _sortByAssetCount;
|
static const AlbumSortFn assetCount = _sortByAssetCount;
|
||||||
static List<Album> _sortByAssetCount(List<Album> albums, bool isReverse) {
|
static List<RemoteAlbum> _sortByAssetCount(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
) {
|
||||||
final sorted =
|
final sorted =
|
||||||
albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
|
albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount));
|
||||||
return (isReverse ? sorted.reversed : sorted).toList();
|
return (isReverse ? sorted.reversed : sorted).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static const AlbumSortFn mostRecent = _sortByMostRecent;
|
static const AlbumSortFn mostRecent = _sortByMostRecent;
|
||||||
static List<Album> _sortByMostRecent(List<Album> albums, bool isReverse) {
|
static List<RemoteAlbum> _sortByMostRecent(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
) {
|
||||||
final sorted = albums.sorted((a, b) {
|
final sorted = albums.sorted((a, b) {
|
||||||
// For most recent, we sort by updatedAt in descending order
|
// For most recent, we sort by updatedAt in descending order
|
||||||
return b.updatedAt.compareTo(a.updatedAt);
|
return b.updatedAt.compareTo(a.updatedAt);
|
||||||
@@ -41,7 +59,10 @@ class _RemoteAlbumSortHandlers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const AlbumSortFn mostOldest = _sortByMostOldest;
|
static const AlbumSortFn mostOldest = _sortByMostOldest;
|
||||||
static List<Album> _sortByMostOldest(List<Album> albums, bool isReverse) {
|
static List<RemoteAlbum> _sortByMostOldest(
|
||||||
|
List<RemoteAlbum> albums,
|
||||||
|
bool isReverse,
|
||||||
|
) {
|
||||||
final sorted = albums.sorted((a, b) {
|
final sorted = albums.sorted((a, b) {
|
||||||
// For oldest, we sort by createdAt in ascending order
|
// For oldest, we sort by createdAt in ascending order
|
||||||
return a.createdAt.compareTo(b.createdAt);
|
return a.createdAt.compareTo(b.createdAt);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
@@ -185,7 +186,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
child: action,
|
child: action,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (kDebugMode || kProfileMode)
|
if (kDebugMode || kProfileMode || appFlavor == 'beta')
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.science_rounded),
|
icon: const Icon(Icons.science_rounded),
|
||||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||||
|
|||||||
@@ -73,7 +73,15 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => ref.read(backgroundSyncProvider).syncRemote(),
|
onPressed: () {
|
||||||
|
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
|
ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
() => ref.read(backgroundSyncProvider).hashAssets(),
|
||||||
|
);
|
||||||
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.sync,
|
Icons.sync,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
|
class MesmerizingSliverAppBar extends ConsumerStatefulWidget {
|
||||||
|
const MesmerizingSliverAppBar({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.icon = Icons.camera,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<MesmerizingSliverAppBar> createState() =>
|
||||||
|
_MesmerizingSliverAppBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MesmerizingSliverAppBarState
|
||||||
|
extends ConsumerState<MesmerizingSliverAppBar> {
|
||||||
|
double _scrollProgress = 0.0;
|
||||||
|
|
||||||
|
double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) {
|
||||||
|
if (settings?.maxExtent == null || settings?.minExtent == null) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaExtent = settings!.maxExtent - settings.minExtent;
|
||||||
|
if (deltaExtent <= 0.0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent)
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isMultiSelectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
|
||||||
|
return isMultiSelectEnabled
|
||||||
|
? SliverToBoxAdapter(
|
||||||
|
child: switch (_scrollProgress) {
|
||||||
|
< 0.8 => const SizedBox(height: 120),
|
||||||
|
_ => const SizedBox(height: 352),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SliverAppBar(
|
||||||
|
expandedHeight: 300.0,
|
||||||
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
|
snap: false,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? Icons.arrow_back_ios_new_rounded
|
||||||
|
: Icons.arrow_back,
|
||||||
|
color: Color.lerp(
|
||||||
|
Colors.white,
|
||||||
|
context.primaryColor,
|
||||||
|
_scrollProgress,
|
||||||
|
),
|
||||||
|
shadows: [
|
||||||
|
_scrollProgress < 0.95
|
||||||
|
? Shadow(
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
blurRadius: 5,
|
||||||
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
|
)
|
||||||
|
: const Shadow(
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
blurRadius: 0,
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
flexibleSpace: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final settings = context.dependOnInheritedWidgetOfExactType<
|
||||||
|
FlexibleSpaceBarSettings>();
|
||||||
|
final scrollProgress = _calculateScrollProgress(settings);
|
||||||
|
|
||||||
|
// Update scroll progress for the leading button
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && _scrollProgress != scrollProgress) {
|
||||||
|
setState(() {
|
||||||
|
_scrollProgress = scrollProgress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FlexibleSpaceBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: scrollProgress > 0.95
|
||||||
|
? Text(
|
||||||
|
widget.title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
background: _ExpandedBackground(
|
||||||
|
scrollProgress: scrollProgress,
|
||||||
|
title: widget.title,
|
||||||
|
icon: widget.icon,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpandedBackground extends ConsumerStatefulWidget {
|
||||||
|
final double scrollProgress;
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const _ExpandedBackground({
|
||||||
|
required this.scrollProgress,
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_ExpandedBackground> createState() =>
|
||||||
|
_ExpandedBackgroundState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _slideController;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_slideController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0, 1.5),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _slideController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
if (mounted) {
|
||||||
|
_slideController.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_slideController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final timelineService = ref.watch(timelineServiceProvider);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Transform.translate(
|
||||||
|
offset: Offset(0, widget.scrollProgress * 50),
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 1.4 - (widget.scrollProgress * 0.2),
|
||||||
|
child: _RandomAssetBackground(
|
||||||
|
timelineService: timelineService,
|
||||||
|
icon: widget.icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.black.withValues(
|
||||||
|
alpha: 0.6 + (widget.scrollProgress * 0.2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.65, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
maxLines: 1,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
blurRadius: 12,
|
||||||
|
color: Colors.black45,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: const _ItemCountText(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ItemCountText extends ConsumerStatefulWidget {
|
||||||
|
const _ItemCountText();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_ItemCountText> createState() => _ItemCountTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ItemCountTextState extends ConsumerState<_ItemCountText> {
|
||||||
|
StreamSubscription? _reloadSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_reloadSubscription =
|
||||||
|
EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_reloadSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final assetCount = ref.watch(
|
||||||
|
timelineServiceProvider.select((s) => s.totalAssets),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
'items_count'.t(
|
||||||
|
context: context,
|
||||||
|
args: {"count": assetCount},
|
||||||
|
),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
// letterSpacing: 0.2,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
const Shadow(
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
blurRadius: 6,
|
||||||
|
color: Colors.black45,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RandomAssetBackground extends StatefulWidget {
|
||||||
|
final TimelineService timelineService;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const _RandomAssetBackground({
|
||||||
|
required this.timelineService,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RandomAssetBackgroundState extends State<_RandomAssetBackground>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late AnimationController _zoomController;
|
||||||
|
late AnimationController _crossFadeController;
|
||||||
|
late Animation<double> _zoomAnimation;
|
||||||
|
late Animation<Offset> _panAnimation;
|
||||||
|
late Animation<double> _crossFadeAnimation;
|
||||||
|
BaseAsset? _currentAsset;
|
||||||
|
BaseAsset? _nextAsset;
|
||||||
|
bool _isZoomingIn = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_zoomController = AnimationController(
|
||||||
|
duration: const Duration(seconds: 12),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_crossFadeController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_zoomAnimation = Tween<double>(
|
||||||
|
begin: 1.0,
|
||||||
|
end: 1.2,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _zoomController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_panAnimation = Tween<Offset>(
|
||||||
|
begin: Offset.zero,
|
||||||
|
end: const Offset(0.5, -0.5),
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _zoomController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_crossFadeAnimation = Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: 1.0,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _crossFadeController,
|
||||||
|
curve: Curves.easeInOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future.delayed(
|
||||||
|
Durations.medium1,
|
||||||
|
() => _loadFirstAsset(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_zoomController.dispose();
|
||||||
|
_crossFadeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startAnimationCycle() {
|
||||||
|
if (_isZoomingIn) {
|
||||||
|
_zoomController.forward().then((_) {
|
||||||
|
_loadNextAsset();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_zoomController.reverse().then((_) {
|
||||||
|
_loadNextAsset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFirstAsset() async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.timelineService.totalAssets == 0) {
|
||||||
|
setState(() {
|
||||||
|
_currentAsset = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_currentAsset = widget.timelineService.getRandomAsset();
|
||||||
|
});
|
||||||
|
|
||||||
|
await _crossFadeController.forward();
|
||||||
|
|
||||||
|
if (_zoomController.status == AnimationStatus.dismissed) {
|
||||||
|
if (_isZoomingIn) {
|
||||||
|
_zoomController.reset();
|
||||||
|
} else {
|
||||||
|
_zoomController.value = 1.0;
|
||||||
|
}
|
||||||
|
_startAnimationCycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadNextAsset() async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (widget.timelineService.totalAssets > 1) {
|
||||||
|
// Load next asset while keeping current one visible
|
||||||
|
final nextAsset = widget.timelineService.getRandomAsset();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_nextAsset = nextAsset;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _crossFadeController.reverse();
|
||||||
|
setState(() {
|
||||||
|
_currentAsset = _nextAsset;
|
||||||
|
_nextAsset = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
_crossFadeController.value = 1.0;
|
||||||
|
|
||||||
|
_isZoomingIn = !_isZoomingIn;
|
||||||
|
|
||||||
|
_startAnimationCycle();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_zoomController.reset();
|
||||||
|
_startAnimationCycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.timelineService.totalAssets == 0) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: Listenable.merge(
|
||||||
|
[_zoomAnimation, _panAnimation, _crossFadeAnimation],
|
||||||
|
),
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.scale(
|
||||||
|
scale: _zoomAnimation.value,
|
||||||
|
filterQuality: FilterQuality.low,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: _panAnimation.value,
|
||||||
|
filterQuality: FilterQuality.low,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
// Current image
|
||||||
|
if (_currentAsset != null)
|
||||||
|
Opacity(
|
||||||
|
opacity: _crossFadeAnimation.value,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Image(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
image: getFullImageProvider(_currentAsset!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
frameBuilder:
|
||||||
|
(context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline_rounded,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.red[300],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (_nextAsset != null)
|
||||||
|
Opacity(
|
||||||
|
opacity: 1.0 - _crossFadeAnimation.value,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Image(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
image: getFullImageProvider(_nextAsset!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
frameBuilder:
|
||||||
|
(context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline_rounded,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.red[300],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -28,4 +28,7 @@ translation:
|
|||||||
dart run easy_localization:generate -S ../i18n
|
dart run easy_localization:generate -S ../i18n
|
||||||
dart run bin/generate_keys.dart
|
dart run bin/generate_keys.dart
|
||||||
dart format lib/generated/codegen_loader.g.dart
|
dart format lib/generated/codegen_loader.g.dart
|
||||||
dart format lib/generated/intl_keys.g.dart
|
dart format lib/generated/intl_keys.g.dart
|
||||||
|
|
||||||
|
build-beta:
|
||||||
|
flutter build apk --flavor beta --release
|
||||||
|
|||||||
Generated
+7
@@ -208,6 +208,7 @@ Class | Method | HTTP request | Description
|
|||||||
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
||||||
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
||||||
*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |
|
*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |
|
||||||
|
*SessionsApi* | [**updateSession**](doc//SessionsApi.md#updatesession) | **PUT** /sessions/{id} |
|
||||||
*SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |
|
*SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |
|
||||||
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
|
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
|
||||||
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
|
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
|
||||||
@@ -449,6 +450,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
|
- [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
|
||||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||||
- [SessionUnlockDto](doc//SessionUnlockDto.md)
|
- [SessionUnlockDto](doc//SessionUnlockDto.md)
|
||||||
|
- [SessionUpdateDto](doc//SessionUpdateDto.md)
|
||||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||||
@@ -481,11 +483,15 @@ Class | Method | HTTP request | Description
|
|||||||
- [SyncMemoryV1](doc//SyncMemoryV1.md)
|
- [SyncMemoryV1](doc//SyncMemoryV1.md)
|
||||||
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
|
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
|
||||||
- [SyncPartnerV1](doc//SyncPartnerV1.md)
|
- [SyncPartnerV1](doc//SyncPartnerV1.md)
|
||||||
|
- [SyncPersonDeleteV1](doc//SyncPersonDeleteV1.md)
|
||||||
|
- [SyncPersonV1](doc//SyncPersonV1.md)
|
||||||
- [SyncRequestType](doc//SyncRequestType.md)
|
- [SyncRequestType](doc//SyncRequestType.md)
|
||||||
- [SyncStackDeleteV1](doc//SyncStackDeleteV1.md)
|
- [SyncStackDeleteV1](doc//SyncStackDeleteV1.md)
|
||||||
- [SyncStackV1](doc//SyncStackV1.md)
|
- [SyncStackV1](doc//SyncStackV1.md)
|
||||||
- [SyncStreamDto](doc//SyncStreamDto.md)
|
- [SyncStreamDto](doc//SyncStreamDto.md)
|
||||||
- [SyncUserDeleteV1](doc//SyncUserDeleteV1.md)
|
- [SyncUserDeleteV1](doc//SyncUserDeleteV1.md)
|
||||||
|
- [SyncUserMetadataDeleteV1](doc//SyncUserMetadataDeleteV1.md)
|
||||||
|
- [SyncUserMetadataV1](doc//SyncUserMetadataV1.md)
|
||||||
- [SyncUserV1](doc//SyncUserV1.md)
|
- [SyncUserV1](doc//SyncUserV1.md)
|
||||||
- [SystemConfigBackupsDto](doc//SystemConfigBackupsDto.md)
|
- [SystemConfigBackupsDto](doc//SystemConfigBackupsDto.md)
|
||||||
- [SystemConfigDto](doc//SystemConfigDto.md)
|
- [SystemConfigDto](doc//SystemConfigDto.md)
|
||||||
@@ -503,6 +509,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
|
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
|
||||||
- [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md)
|
- [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md)
|
||||||
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
|
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
|
||||||
|
- [SystemConfigNightlyTasksDto](doc//SystemConfigNightlyTasksDto.md)
|
||||||
- [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md)
|
- [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md)
|
||||||
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
|
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
|
||||||
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
|
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
|
||||||
|
|||||||
Generated
+6
@@ -232,6 +232,7 @@ part 'model/session_create_dto.dart';
|
|||||||
part 'model/session_create_response_dto.dart';
|
part 'model/session_create_response_dto.dart';
|
||||||
part 'model/session_response_dto.dart';
|
part 'model/session_response_dto.dart';
|
||||||
part 'model/session_unlock_dto.dart';
|
part 'model/session_unlock_dto.dart';
|
||||||
|
part 'model/session_update_dto.dart';
|
||||||
part 'model/shared_link_create_dto.dart';
|
part 'model/shared_link_create_dto.dart';
|
||||||
part 'model/shared_link_edit_dto.dart';
|
part 'model/shared_link_edit_dto.dart';
|
||||||
part 'model/shared_link_response_dto.dart';
|
part 'model/shared_link_response_dto.dart';
|
||||||
@@ -264,11 +265,15 @@ part 'model/sync_memory_delete_v1.dart';
|
|||||||
part 'model/sync_memory_v1.dart';
|
part 'model/sync_memory_v1.dart';
|
||||||
part 'model/sync_partner_delete_v1.dart';
|
part 'model/sync_partner_delete_v1.dart';
|
||||||
part 'model/sync_partner_v1.dart';
|
part 'model/sync_partner_v1.dart';
|
||||||
|
part 'model/sync_person_delete_v1.dart';
|
||||||
|
part 'model/sync_person_v1.dart';
|
||||||
part 'model/sync_request_type.dart';
|
part 'model/sync_request_type.dart';
|
||||||
part 'model/sync_stack_delete_v1.dart';
|
part 'model/sync_stack_delete_v1.dart';
|
||||||
part 'model/sync_stack_v1.dart';
|
part 'model/sync_stack_v1.dart';
|
||||||
part 'model/sync_stream_dto.dart';
|
part 'model/sync_stream_dto.dart';
|
||||||
part 'model/sync_user_delete_v1.dart';
|
part 'model/sync_user_delete_v1.dart';
|
||||||
|
part 'model/sync_user_metadata_delete_v1.dart';
|
||||||
|
part 'model/sync_user_metadata_v1.dart';
|
||||||
part 'model/sync_user_v1.dart';
|
part 'model/sync_user_v1.dart';
|
||||||
part 'model/system_config_backups_dto.dart';
|
part 'model/system_config_backups_dto.dart';
|
||||||
part 'model/system_config_dto.dart';
|
part 'model/system_config_dto.dart';
|
||||||
@@ -286,6 +291,7 @@ part 'model/system_config_machine_learning_dto.dart';
|
|||||||
part 'model/system_config_map_dto.dart';
|
part 'model/system_config_map_dto.dart';
|
||||||
part 'model/system_config_metadata_dto.dart';
|
part 'model/system_config_metadata_dto.dart';
|
||||||
part 'model/system_config_new_version_check_dto.dart';
|
part 'model/system_config_new_version_check_dto.dart';
|
||||||
|
part 'model/system_config_nightly_tasks_dto.dart';
|
||||||
part 'model/system_config_notifications_dto.dart';
|
part 'model/system_config_notifications_dto.dart';
|
||||||
part 'model/system_config_o_auth_dto.dart';
|
part 'model/system_config_o_auth_dto.dart';
|
||||||
part 'model/system_config_password_login_dto.dart';
|
part 'model/system_config_password_login_dto.dart';
|
||||||
|
|||||||
Generated
+52
@@ -219,4 +219,56 @@ class SessionsApi {
|
|||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /sessions/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [SessionUpdateDto] sessionUpdateDto (required):
|
||||||
|
Future<Response> updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/sessions/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = sessionUpdateDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [SessionUpdateDto] sessionUpdateDto (required):
|
||||||
|
Future<SessionResponseDto?> updateSession(String id, SessionUpdateDto sessionUpdateDto,) async {
|
||||||
|
final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SessionResponseDto',) as SessionResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
@@ -520,6 +520,8 @@ class ApiClient {
|
|||||||
return SessionResponseDto.fromJson(value);
|
return SessionResponseDto.fromJson(value);
|
||||||
case 'SessionUnlockDto':
|
case 'SessionUnlockDto':
|
||||||
return SessionUnlockDto.fromJson(value);
|
return SessionUnlockDto.fromJson(value);
|
||||||
|
case 'SessionUpdateDto':
|
||||||
|
return SessionUpdateDto.fromJson(value);
|
||||||
case 'SharedLinkCreateDto':
|
case 'SharedLinkCreateDto':
|
||||||
return SharedLinkCreateDto.fromJson(value);
|
return SharedLinkCreateDto.fromJson(value);
|
||||||
case 'SharedLinkEditDto':
|
case 'SharedLinkEditDto':
|
||||||
@@ -584,6 +586,10 @@ class ApiClient {
|
|||||||
return SyncPartnerDeleteV1.fromJson(value);
|
return SyncPartnerDeleteV1.fromJson(value);
|
||||||
case 'SyncPartnerV1':
|
case 'SyncPartnerV1':
|
||||||
return SyncPartnerV1.fromJson(value);
|
return SyncPartnerV1.fromJson(value);
|
||||||
|
case 'SyncPersonDeleteV1':
|
||||||
|
return SyncPersonDeleteV1.fromJson(value);
|
||||||
|
case 'SyncPersonV1':
|
||||||
|
return SyncPersonV1.fromJson(value);
|
||||||
case 'SyncRequestType':
|
case 'SyncRequestType':
|
||||||
return SyncRequestTypeTypeTransformer().decode(value);
|
return SyncRequestTypeTypeTransformer().decode(value);
|
||||||
case 'SyncStackDeleteV1':
|
case 'SyncStackDeleteV1':
|
||||||
@@ -594,6 +600,10 @@ class ApiClient {
|
|||||||
return SyncStreamDto.fromJson(value);
|
return SyncStreamDto.fromJson(value);
|
||||||
case 'SyncUserDeleteV1':
|
case 'SyncUserDeleteV1':
|
||||||
return SyncUserDeleteV1.fromJson(value);
|
return SyncUserDeleteV1.fromJson(value);
|
||||||
|
case 'SyncUserMetadataDeleteV1':
|
||||||
|
return SyncUserMetadataDeleteV1.fromJson(value);
|
||||||
|
case 'SyncUserMetadataV1':
|
||||||
|
return SyncUserMetadataV1.fromJson(value);
|
||||||
case 'SyncUserV1':
|
case 'SyncUserV1':
|
||||||
return SyncUserV1.fromJson(value);
|
return SyncUserV1.fromJson(value);
|
||||||
case 'SystemConfigBackupsDto':
|
case 'SystemConfigBackupsDto':
|
||||||
@@ -628,6 +638,8 @@ class ApiClient {
|
|||||||
return SystemConfigMetadataDto.fromJson(value);
|
return SystemConfigMetadataDto.fromJson(value);
|
||||||
case 'SystemConfigNewVersionCheckDto':
|
case 'SystemConfigNewVersionCheckDto':
|
||||||
return SystemConfigNewVersionCheckDto.fromJson(value);
|
return SystemConfigNewVersionCheckDto.fromJson(value);
|
||||||
|
case 'SystemConfigNightlyTasksDto':
|
||||||
|
return SystemConfigNightlyTasksDto.fromJson(value);
|
||||||
case 'SystemConfigNotificationsDto':
|
case 'SystemConfigNotificationsDto':
|
||||||
return SystemConfigNotificationsDto.fromJson(value);
|
return SystemConfigNotificationsDto.fromJson(value);
|
||||||
case 'SystemConfigOAuthDto':
|
case 'SystemConfigOAuthDto':
|
||||||
|
|||||||
+9
-1
@@ -19,6 +19,7 @@ class SessionCreateResponseDto {
|
|||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.isPendingSyncReset,
|
||||||
required this.token,
|
required this.token,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
@@ -41,6 +42,8 @@ class SessionCreateResponseDto {
|
|||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
|
bool isPendingSyncReset;
|
||||||
|
|
||||||
String token;
|
String token;
|
||||||
|
|
||||||
String updatedAt;
|
String updatedAt;
|
||||||
@@ -53,6 +56,7 @@ class SessionCreateResponseDto {
|
|||||||
other.deviceType == deviceType &&
|
other.deviceType == deviceType &&
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.isPendingSyncReset == isPendingSyncReset &&
|
||||||
other.token == token &&
|
other.token == token &&
|
||||||
other.updatedAt == updatedAt;
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
@@ -65,11 +69,12 @@ class SessionCreateResponseDto {
|
|||||||
(deviceType.hashCode) +
|
(deviceType.hashCode) +
|
||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
|
(isPendingSyncReset.hashCode) +
|
||||||
(token.hashCode) +
|
(token.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]';
|
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -83,6 +88,7 @@ class SessionCreateResponseDto {
|
|||||||
// json[r'expiresAt'] = null;
|
// json[r'expiresAt'] = null;
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
|
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||||
json[r'token'] = this.token;
|
json[r'token'] = this.token;
|
||||||
json[r'updatedAt'] = this.updatedAt;
|
json[r'updatedAt'] = this.updatedAt;
|
||||||
return json;
|
return json;
|
||||||
@@ -103,6 +109,7 @@ class SessionCreateResponseDto {
|
|||||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
||||||
token: mapValueOfType<String>(json, r'token')!,
|
token: mapValueOfType<String>(json, r'token')!,
|
||||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||||
);
|
);
|
||||||
@@ -157,6 +164,7 @@ class SessionCreateResponseDto {
|
|||||||
'deviceOS',
|
'deviceOS',
|
||||||
'deviceType',
|
'deviceType',
|
||||||
'id',
|
'id',
|
||||||
|
'isPendingSyncReset',
|
||||||
'token',
|
'token',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -19,6 +19,7 @@ class SessionResponseDto {
|
|||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.isPendingSyncReset,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@ class SessionResponseDto {
|
|||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
|
bool isPendingSyncReset;
|
||||||
|
|
||||||
String updatedAt;
|
String updatedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -50,6 +53,7 @@ class SessionResponseDto {
|
|||||||
other.deviceType == deviceType &&
|
other.deviceType == deviceType &&
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.isPendingSyncReset == isPendingSyncReset &&
|
||||||
other.updatedAt == updatedAt;
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,10 +65,11 @@ class SessionResponseDto {
|
|||||||
(deviceType.hashCode) +
|
(deviceType.hashCode) +
|
||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
|
(isPendingSyncReset.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]';
|
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -78,6 +83,7 @@ class SessionResponseDto {
|
|||||||
// json[r'expiresAt'] = null;
|
// json[r'expiresAt'] = null;
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
|
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||||
json[r'updatedAt'] = this.updatedAt;
|
json[r'updatedAt'] = this.updatedAt;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -97,6 +103,7 @@ class SessionResponseDto {
|
|||||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
||||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,7 @@ class SessionResponseDto {
|
|||||||
'deviceOS',
|
'deviceOS',
|
||||||
'deviceType',
|
'deviceType',
|
||||||
'id',
|
'id',
|
||||||
|
'isPendingSyncReset',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SessionUpdateDto {
|
||||||
|
/// Returns a new [SessionUpdateDto] instance.
|
||||||
|
SessionUpdateDto({
|
||||||
|
this.isPendingSyncReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
bool? isPendingSyncReset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SessionUpdateDto &&
|
||||||
|
other.isPendingSyncReset == isPendingSyncReset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(isPendingSyncReset == null ? 0 : isPendingSyncReset!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SessionUpdateDto[isPendingSyncReset=$isPendingSyncReset]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (this.isPendingSyncReset != null) {
|
||||||
|
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||||
|
} else {
|
||||||
|
// json[r'isPendingSyncReset'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SessionUpdateDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SessionUpdateDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SessionUpdateDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SessionUpdateDto(
|
||||||
|
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SessionUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SessionUpdateDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SessionUpdateDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SessionUpdateDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SessionUpdateDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SessionUpdateDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SessionUpdateDto-objects as value to a dart map
|
||||||
|
static Map<String, List<SessionUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SessionUpdateDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SessionUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+25
-1
@@ -20,9 +20,11 @@ class SyncAssetV1 {
|
|||||||
required this.fileModifiedAt,
|
required this.fileModifiedAt,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
|
required this.livePhotoVideoId,
|
||||||
required this.localDateTime,
|
required this.localDateTime,
|
||||||
required this.originalFileName,
|
required this.originalFileName,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
|
required this.stackId,
|
||||||
required this.thumbhash,
|
required this.thumbhash,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.visibility,
|
required this.visibility,
|
||||||
@@ -42,12 +44,16 @@ class SyncAssetV1 {
|
|||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
DateTime? localDateTime;
|
DateTime? localDateTime;
|
||||||
|
|
||||||
String originalFileName;
|
String originalFileName;
|
||||||
|
|
||||||
String ownerId;
|
String ownerId;
|
||||||
|
|
||||||
|
String? stackId;
|
||||||
|
|
||||||
String? thumbhash;
|
String? thumbhash;
|
||||||
|
|
||||||
AssetTypeEnum type;
|
AssetTypeEnum type;
|
||||||
@@ -63,9 +69,11 @@ class SyncAssetV1 {
|
|||||||
other.fileModifiedAt == fileModifiedAt &&
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
|
other.livePhotoVideoId == livePhotoVideoId &&
|
||||||
other.localDateTime == localDateTime &&
|
other.localDateTime == localDateTime &&
|
||||||
other.originalFileName == originalFileName &&
|
other.originalFileName == originalFileName &&
|
||||||
other.ownerId == ownerId &&
|
other.ownerId == ownerId &&
|
||||||
|
other.stackId == stackId &&
|
||||||
other.thumbhash == thumbhash &&
|
other.thumbhash == thumbhash &&
|
||||||
other.type == type &&
|
other.type == type &&
|
||||||
other.visibility == visibility;
|
other.visibility == visibility;
|
||||||
@@ -80,15 +88,17 @@ class SyncAssetV1 {
|
|||||||
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
|
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||||
(localDateTime == null ? 0 : localDateTime!.hashCode) +
|
(localDateTime == null ? 0 : localDateTime!.hashCode) +
|
||||||
(originalFileName.hashCode) +
|
(originalFileName.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
|
(stackId == null ? 0 : stackId!.hashCode) +
|
||||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(visibility.hashCode);
|
(visibility.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
|
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -115,6 +125,11 @@ class SyncAssetV1 {
|
|||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
|
if (this.livePhotoVideoId != null) {
|
||||||
|
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
|
||||||
|
} else {
|
||||||
|
// json[r'livePhotoVideoId'] = null;
|
||||||
|
}
|
||||||
if (this.localDateTime != null) {
|
if (this.localDateTime != null) {
|
||||||
json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String();
|
json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String();
|
||||||
} else {
|
} else {
|
||||||
@@ -122,6 +137,11 @@ class SyncAssetV1 {
|
|||||||
}
|
}
|
||||||
json[r'originalFileName'] = this.originalFileName;
|
json[r'originalFileName'] = this.originalFileName;
|
||||||
json[r'ownerId'] = this.ownerId;
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
if (this.stackId != null) {
|
||||||
|
json[r'stackId'] = this.stackId;
|
||||||
|
} else {
|
||||||
|
// json[r'stackId'] = null;
|
||||||
|
}
|
||||||
if (this.thumbhash != null) {
|
if (this.thumbhash != null) {
|
||||||
json[r'thumbhash'] = this.thumbhash;
|
json[r'thumbhash'] = this.thumbhash;
|
||||||
} else {
|
} else {
|
||||||
@@ -148,9 +168,11 @@ class SyncAssetV1 {
|
|||||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
localDateTime: mapDateTime(json, r'localDateTime', r''),
|
localDateTime: mapDateTime(json, r'localDateTime', r''),
|
||||||
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
stackId: mapValueOfType<String>(json, r'stackId'),
|
||||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||||
@@ -208,9 +230,11 @@ class SyncAssetV1 {
|
|||||||
'fileModifiedAt',
|
'fileModifiedAt',
|
||||||
'id',
|
'id',
|
||||||
'isFavorite',
|
'isFavorite',
|
||||||
|
'livePhotoVideoId',
|
||||||
'localDateTime',
|
'localDateTime',
|
||||||
'originalFileName',
|
'originalFileName',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
|
'stackId',
|
||||||
'thumbhash',
|
'thumbhash',
|
||||||
'type',
|
'type',
|
||||||
'visibility',
|
'visibility',
|
||||||
|
|||||||
+15
@@ -56,7 +56,12 @@ class SyncEntityType {
|
|||||||
static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1');
|
static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1');
|
||||||
static const stackV1 = SyncEntityType._(r'StackV1');
|
static const stackV1 = SyncEntityType._(r'StackV1');
|
||||||
static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1');
|
static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1');
|
||||||
|
static const personV1 = SyncEntityType._(r'PersonV1');
|
||||||
|
static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1');
|
||||||
|
static const userMetadataV1 = SyncEntityType._(r'UserMetadataV1');
|
||||||
|
static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1');
|
||||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||||
|
static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
static const values = <SyncEntityType>[
|
static const values = <SyncEntityType>[
|
||||||
@@ -93,7 +98,12 @@ class SyncEntityType {
|
|||||||
memoryToAssetDeleteV1,
|
memoryToAssetDeleteV1,
|
||||||
stackV1,
|
stackV1,
|
||||||
stackDeleteV1,
|
stackDeleteV1,
|
||||||
|
personV1,
|
||||||
|
personDeleteV1,
|
||||||
|
userMetadataV1,
|
||||||
|
userMetadataDeleteV1,
|
||||||
syncAckV1,
|
syncAckV1,
|
||||||
|
syncResetV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
||||||
@@ -165,7 +175,12 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1;
|
case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1;
|
||||||
case r'StackV1': return SyncEntityType.stackV1;
|
case r'StackV1': return SyncEntityType.stackV1;
|
||||||
case r'StackDeleteV1': return SyncEntityType.stackDeleteV1;
|
case r'StackDeleteV1': return SyncEntityType.stackDeleteV1;
|
||||||
|
case r'PersonV1': return SyncEntityType.personV1;
|
||||||
|
case r'PersonDeleteV1': return SyncEntityType.personDeleteV1;
|
||||||
|
case r'UserMetadataV1': return SyncEntityType.userMetadataV1;
|
||||||
|
case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1;
|
||||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||||
|
case r'SyncResetV1': return SyncEntityType.syncResetV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncPersonDeleteV1 {
|
||||||
|
/// Returns a new [SyncPersonDeleteV1] instance.
|
||||||
|
SyncPersonDeleteV1({
|
||||||
|
required this.personId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String personId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncPersonDeleteV1 &&
|
||||||
|
other.personId == personId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(personId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncPersonDeleteV1[personId=$personId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'personId'] = this.personId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncPersonDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncPersonDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncPersonDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncPersonDeleteV1(
|
||||||
|
personId: mapValueOfType<String>(json, r'personId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncPersonDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncPersonDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncPersonDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncPersonDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncPersonDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncPersonDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncPersonDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncPersonDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncPersonDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncPersonDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'personId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncPersonV1 {
|
||||||
|
/// Returns a new [SyncPersonV1] instance.
|
||||||
|
SyncPersonV1({
|
||||||
|
required this.birthDate,
|
||||||
|
required this.color,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.faceAssetId,
|
||||||
|
required this.id,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.isHidden,
|
||||||
|
required this.name,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.thumbnailPath,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime? birthDate;
|
||||||
|
|
||||||
|
String? color;
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
String? faceAssetId;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
bool isFavorite;
|
||||||
|
|
||||||
|
bool isHidden;
|
||||||
|
|
||||||
|
String name;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
String thumbnailPath;
|
||||||
|
|
||||||
|
DateTime updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncPersonV1 &&
|
||||||
|
other.birthDate == birthDate &&
|
||||||
|
other.color == color &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.faceAssetId == faceAssetId &&
|
||||||
|
other.id == id &&
|
||||||
|
other.isFavorite == isFavorite &&
|
||||||
|
other.isHidden == isHidden &&
|
||||||
|
other.name == name &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.thumbnailPath == thumbnailPath &&
|
||||||
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||||
|
(color == null ? 0 : color!.hashCode) +
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(faceAssetId == null ? 0 : faceAssetId!.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isFavorite.hashCode) +
|
||||||
|
(isHidden.hashCode) +
|
||||||
|
(name.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(thumbnailPath.hashCode) +
|
||||||
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncPersonV1[birthDate=$birthDate, color=$color, createdAt=$createdAt, faceAssetId=$faceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, ownerId=$ownerId, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (this.birthDate != null) {
|
||||||
|
json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'birthDate'] = null;
|
||||||
|
}
|
||||||
|
if (this.color != null) {
|
||||||
|
json[r'color'] = this.color;
|
||||||
|
} else {
|
||||||
|
// json[r'color'] = null;
|
||||||
|
}
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
if (this.faceAssetId != null) {
|
||||||
|
json[r'faceAssetId'] = this.faceAssetId;
|
||||||
|
} else {
|
||||||
|
// json[r'faceAssetId'] = null;
|
||||||
|
}
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
|
json[r'isHidden'] = this.isHidden;
|
||||||
|
json[r'name'] = this.name;
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
json[r'thumbnailPath'] = this.thumbnailPath;
|
||||||
|
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncPersonV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncPersonV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncPersonV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncPersonV1(
|
||||||
|
birthDate: mapDateTime(json, r'birthDate', r''),
|
||||||
|
color: mapValueOfType<String>(json, r'color'),
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
faceAssetId: mapValueOfType<String>(json, r'faceAssetId'),
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
|
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||||
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||||
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncPersonV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncPersonV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncPersonV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncPersonV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncPersonV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncPersonV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncPersonV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncPersonV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncPersonV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncPersonV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'birthDate',
|
||||||
|
'color',
|
||||||
|
'createdAt',
|
||||||
|
'faceAssetId',
|
||||||
|
'id',
|
||||||
|
'isFavorite',
|
||||||
|
'isHidden',
|
||||||
|
'name',
|
||||||
|
'ownerId',
|
||||||
|
'thumbnailPath',
|
||||||
|
'updatedAt',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
+6
@@ -38,6 +38,8 @@ class SyncRequestType {
|
|||||||
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
||||||
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
||||||
static const usersV1 = SyncRequestType._(r'UsersV1');
|
static const usersV1 = SyncRequestType._(r'UsersV1');
|
||||||
|
static const peopleV1 = SyncRequestType._(r'PeopleV1');
|
||||||
|
static const userMetadataV1 = SyncRequestType._(r'UserMetadataV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncRequestType].
|
/// List of all possible values in this [enum][SyncRequestType].
|
||||||
static const values = <SyncRequestType>[
|
static const values = <SyncRequestType>[
|
||||||
@@ -56,6 +58,8 @@ class SyncRequestType {
|
|||||||
partnerStacksV1,
|
partnerStacksV1,
|
||||||
stacksV1,
|
stacksV1,
|
||||||
usersV1,
|
usersV1,
|
||||||
|
peopleV1,
|
||||||
|
userMetadataV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
||||||
@@ -109,6 +113,8 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
||||||
case r'StacksV1': return SyncRequestType.stacksV1;
|
case r'StacksV1': return SyncRequestType.stacksV1;
|
||||||
case r'UsersV1': return SyncRequestType.usersV1;
|
case r'UsersV1': return SyncRequestType.usersV1;
|
||||||
|
case r'PeopleV1': return SyncRequestType.peopleV1;
|
||||||
|
case r'UserMetadataV1': return SyncRequestType.userMetadataV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
+18
-1
@@ -13,25 +13,41 @@ part of openapi.api;
|
|||||||
class SyncStreamDto {
|
class SyncStreamDto {
|
||||||
/// Returns a new [SyncStreamDto] instance.
|
/// Returns a new [SyncStreamDto] instance.
|
||||||
SyncStreamDto({
|
SyncStreamDto({
|
||||||
|
this.reset,
|
||||||
this.types = const [],
|
this.types = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
bool? reset;
|
||||||
|
|
||||||
List<SyncRequestType> types;
|
List<SyncRequestType> types;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto &&
|
||||||
|
other.reset == reset &&
|
||||||
_deepEquality.equals(other.types, types);
|
_deepEquality.equals(other.types, types);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(reset == null ? 0 : reset!.hashCode) +
|
||||||
(types.hashCode);
|
(types.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SyncStreamDto[types=$types]';
|
String toString() => 'SyncStreamDto[reset=$reset, types=$types]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.reset != null) {
|
||||||
|
json[r'reset'] = this.reset;
|
||||||
|
} else {
|
||||||
|
// json[r'reset'] = null;
|
||||||
|
}
|
||||||
json[r'types'] = this.types;
|
json[r'types'] = this.types;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -45,6 +61,7 @@ class SyncStreamDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return SyncStreamDto(
|
return SyncStreamDto(
|
||||||
|
reset: mapValueOfType<bool>(json, r'reset'),
|
||||||
types: SyncRequestType.listFromJson(json[r'types']),
|
types: SyncRequestType.listFromJson(json[r'types']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncUserMetadataDeleteV1 {
|
||||||
|
/// Returns a new [SyncUserMetadataDeleteV1] instance.
|
||||||
|
SyncUserMetadataDeleteV1({
|
||||||
|
required this.key,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String key;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataDeleteV1 &&
|
||||||
|
other.key == key &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(key.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncUserMetadataDeleteV1[key=$key, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'key'] = this.key;
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncUserMetadataDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncUserMetadataDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncUserMetadataDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncUserMetadataDeleteV1(
|
||||||
|
key: mapValueOfType<String>(json, r'key')!,
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncUserMetadataDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncUserMetadataDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncUserMetadataDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncUserMetadataDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncUserMetadataDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncUserMetadataDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncUserMetadataDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncUserMetadataDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncUserMetadataDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncUserMetadataDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'key',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user