mirror of
https://github.com/immich-app/immich.git
synced 2025-07-07 10:14:08 -04:00
Merge remote-tracking branch 'origin/main' into lighter_buckets_web
This commit is contained in:
commit
ffda7364dd
78
.vscode/settings.json
vendored
78
.vscode/settings.json
vendored
@ -1,45 +1,63 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.tabSize": 2,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[svelte]": {
|
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
},
|
},
|
||||||
"svelte.enable-ts-plugin": true,
|
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
"[dart]": {
|
"[dart]": {
|
||||||
|
"editor.defaultFormatter": "Dart-Code.dart-code",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.selectionHighlight": false,
|
"editor.selectionHighlight": false,
|
||||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
"editor.suggestSelection": "first",
|
"editor.suggestSelection": "first",
|
||||||
"editor.tabCompletion": "onlySnippets",
|
"editor.tabCompletion": "onlySnippets",
|
||||||
"editor.wordBasedSuggestions": "off",
|
"editor.wordBasedSuggestions": "off"
|
||||||
"editor.defaultFormatter": "Dart-Code.dart-code"
|
|
||||||
},
|
},
|
||||||
"cSpell.words": [
|
"[javascript]": {
|
||||||
"immich"
|
"editor.codeActionsOnSave": {
|
||||||
],
|
"source.organizeImports": "explicit",
|
||||||
|
"source.removeUnusedImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.removeUnusedImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.removeUnusedImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"cSpell.words": ["immich"],
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"eslint.validate": ["javascript", "svelte"],
|
||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
|
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||||
}
|
},
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
}
|
}
|
3
Makefile
3
Makefile
@ -17,6 +17,9 @@ e2e:
|
|||||||
prod:
|
prod:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
prod-down:
|
||||||
|
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
|
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.63",
|
"version": "2.2.65",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.63",
|
"version": "2.2.65",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.63",
|
"version": "2.2.65",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
@ -14,14 +14,14 @@ online generators you can use.
|
|||||||
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
|
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
|
||||||
3. Save your selections. Reload the map, and enjoy your custom map style!
|
3. Save your selections. Reload the map, and enjoy your custom map style!
|
||||||
|
|
||||||
## Use Maptiler to build a custom style
|
## Use MapTiler to build a custom style
|
||||||
|
|
||||||
Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
|
Customizing the map style can be done easily using MapTiler, if you do not want to write an entire JSON document by hand.
|
||||||
|
|
||||||
1. Create a free account at https://cloud.maptiler.com
|
1. Create a free account at https://cloud.maptiler.com
|
||||||
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
|
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
|
||||||
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
|
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
|
||||||
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
|
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
|
||||||
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>
|
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. MapTiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>
|
||||||
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
|
6. MapTiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
|
||||||
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
|
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to MapTiler.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Database Queries
|
# Database Queries
|
||||||
|
|
||||||
:::danger
|
:::danger
|
||||||
Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups.
|
Keep in mind that mucking around in the database might set the Moon on fire. Avoid modifying the database directly when possible, and always have current backups.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
@ -252,6 +252,13 @@ const milestones: Item[] = [
|
|||||||
description: 'Browse your photos and videos in their folder structure inside the mobile app',
|
description: 'Browse your photos and videos in their folder structure inside the mobile app',
|
||||||
release: 'v1.130.0',
|
release: 'v1.130.0',
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
icon: mdiStar,
|
||||||
|
iconColor: 'gold',
|
||||||
|
title: '60,000 Stars',
|
||||||
|
description: 'Reached 60K Stars on GitHub!',
|
||||||
|
getDateLabel: withLanguage(new Date(2025, 2, 4)),
|
||||||
|
},
|
||||||
withRelease({
|
withRelease({
|
||||||
icon: mdiTagFaces,
|
icon: mdiTagFaces,
|
||||||
iconColor: 'teal',
|
iconColor: 'teal',
|
||||||
@ -260,13 +267,6 @@ const milestones: Item[] = [
|
|||||||
'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.',
|
'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.',
|
||||||
release: 'v1.127.0',
|
release: 'v1.127.0',
|
||||||
}),
|
}),
|
||||||
{
|
|
||||||
icon: mdiStar,
|
|
||||||
iconColor: 'gold',
|
|
||||||
title: '60,000 Stars',
|
|
||||||
description: 'Reached 60K Stars on GitHub!',
|
|
||||||
getDateLabel: withLanguage(new Date(2025, 2, 4)),
|
|
||||||
},
|
|
||||||
withRelease({
|
withRelease({
|
||||||
icon: mdiLinkEdit,
|
icon: mdiLinkEdit,
|
||||||
iconColor: 'crimson',
|
iconColor: 'crimson',
|
||||||
|
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@ -1,4 +1,12 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.132.3",
|
||||||
|
"url": "https://v1.132.3.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.132.2",
|
||||||
|
"url": "https://v1.132.2.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.132.1",
|
"label": "v1.132.1",
|
||||||
"url": "https://v1.132.1.archive.immich.app"
|
"url": "https://v1.132.1.archive.immich.app"
|
||||||
|
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.63",
|
"version": "2.2.65",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -93,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -215,6 +215,19 @@ describe('/admin/users', () => {
|
|||||||
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
|
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
|
||||||
expect(user).toMatchObject({ email: nonAdmin.userEmail });
|
expect(user).toMatchObject({ email: nonAdmin.userEmail });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the avatar color', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/admin/users/${admin.userId}`)
|
||||||
|
.send({ avatarColor: 'orange' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ avatarColor: 'orange' });
|
||||||
|
|
||||||
|
const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(after).toMatchObject({ avatarColor: 'orange' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /admin/users/:id/preferences', () => {
|
describe('PUT /admin/users/:id/preferences', () => {
|
||||||
@ -240,19 +253,6 @@ describe('/admin/users', () => {
|
|||||||
expect(after).toMatchObject({ memories: { enabled: false } });
|
expect(after).toMatchObject({ memories: { enabled: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the avatar color', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/admin/users/${admin.userId}/preferences`)
|
|
||||||
.send({ avatar: { color: 'orange' } })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ avatar: { color: 'orange' } });
|
|
||||||
|
|
||||||
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(after).toMatchObject({ avatar: { color: 'orange' } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update download archive size', async () => {
|
it('should update download archive size', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/admin/users/${admin.userId}/preferences`)
|
.put(`/admin/users/${admin.userId}/preferences`)
|
||||||
|
@ -139,6 +139,19 @@ describe('/users', () => {
|
|||||||
profileChangedAt: expect.anything(),
|
profileChangedAt: expect.anything(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update avatar color', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/users/me`)
|
||||||
|
.send({ avatarColor: 'blue' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ avatarColor: 'blue' });
|
||||||
|
|
||||||
|
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(after).toMatchObject({ avatarColor: 'blue' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /users/me/preferences', () => {
|
describe('PUT /users/me/preferences', () => {
|
||||||
@ -158,19 +171,6 @@ describe('/users', () => {
|
|||||||
expect(after).toMatchObject({ memories: { enabled: false } });
|
expect(after).toMatchObject({ memories: { enabled: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update avatar color', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/users/me/preferences`)
|
|
||||||
.send({ avatar: { color: 'blue' } })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ avatar: { color: 'blue' } });
|
|
||||||
|
|
||||||
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(after).toMatchObject({ avatar: { color: 'blue' } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require an integer for download archive size', async () => {
|
it('should require an integer for download archive size', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/users/me/preferences`)
|
.put(`/users/me/preferences`)
|
||||||
|
@ -25,7 +25,7 @@ test.describe('Registration', () => {
|
|||||||
|
|
||||||
// login
|
// login
|
||||||
await expect(page).toHaveTitle(/Login/);
|
await expect(page).toHaveTitle(/Login/);
|
||||||
await page.goto('/auth/login');
|
await page.goto('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('admin@immich.app');
|
await page.getByLabel('Email').fill('admin@immich.app');
|
||||||
await page.getByLabel('Password').fill('password');
|
await page.getByLabel('Password').fill('password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
@ -59,7 +59,7 @@ test.describe('Registration', () => {
|
|||||||
await context.clearCookies();
|
await context.clearCookies();
|
||||||
|
|
||||||
// login
|
// login
|
||||||
await page.goto('/auth/login');
|
await page.goto('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||||
await page.getByLabel('Password').fill('password');
|
await page.getByLabel('Password').fill('password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
@ -72,7 +72,7 @@ test.describe('Registration', () => {
|
|||||||
await page.getByRole('button', { name: 'Change password' }).click();
|
await page.getByRole('button', { name: 'Change password' }).click();
|
||||||
|
|
||||||
// login with new password
|
// login with new password
|
||||||
await expect(page).toHaveURL('/auth/login');
|
await expect(page).toHaveURL('/auth/login?autoLaunch=0');
|
||||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||||
await page.getByLabel('Password').fill('new-password');
|
await page.getByLabel('Password').fill('new-password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
@ -21,23 +21,9 @@ test.describe('Photo Viewer', () => {
|
|||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ context, page }) => {
|
||||||
// before each test, login as user
|
// before each test, login as user
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
await page.goto('/photos');
|
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially shows a loading spinner', async ({ page }) => {
|
|
||||||
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
|
|
||||||
// slow down the request for thumbnail, so spinner has chance to show up
|
|
||||||
await new Promise((f) => setTimeout(f, 2000));
|
|
||||||
await route.continue();
|
|
||||||
});
|
|
||||||
await page.goto(`/photos/${asset.id}`);
|
|
||||||
await page.waitForLoadState('load');
|
|
||||||
// this is the spinner
|
|
||||||
await page.waitForSelector('svg[role=status]');
|
|
||||||
await expect(page.getByTestId('loading-spinner')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('loads original photo when zoomed', async ({ page }) => {
|
test('loads original photo when zoomed', async ({ page }) => {
|
||||||
await page.goto(`/photos/${asset.id}`);
|
await page.goto(`/photos/${asset.id}`);
|
||||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
||||||
|
@ -853,10 +853,12 @@
|
|||||||
"failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
|
"failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
|
||||||
"failed_to_load_asset": "Failed to load asset",
|
"failed_to_load_asset": "Failed to load asset",
|
||||||
"failed_to_load_assets": "Failed to load assets",
|
"failed_to_load_assets": "Failed to load assets",
|
||||||
|
"failed_to_load_notifications": "Failed to load notifications",
|
||||||
"failed_to_load_people": "Failed to load people",
|
"failed_to_load_people": "Failed to load people",
|
||||||
"failed_to_remove_product_key": "Failed to remove product key",
|
"failed_to_remove_product_key": "Failed to remove product key",
|
||||||
"failed_to_stack_assets": "Failed to stack assets",
|
"failed_to_stack_assets": "Failed to stack assets",
|
||||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||||
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
"import_path_already_exists": "This import path already exists.",
|
"import_path_already_exists": "This import path already exists.",
|
||||||
"incorrect_email_or_password": "Incorrect email or password",
|
"incorrect_email_or_password": "Incorrect email or password",
|
||||||
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
||||||
@ -1199,6 +1201,9 @@
|
|||||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||||
"map_settings_theme_settings": "Map Theme",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||||
|
"mark_as_read": "Mark as read",
|
||||||
|
"mark_all_as_read": "Mark all as read",
|
||||||
|
"marked_all_as_read": "Marked all as read",
|
||||||
"matches": "Matches",
|
"matches": "Matches",
|
||||||
"media_type": "Media type",
|
"media_type": "Media type",
|
||||||
"memories": "Memories",
|
"memories": "Memories",
|
||||||
@ -1260,6 +1265,7 @@
|
|||||||
"no_places": "No places",
|
"no_places": "No places",
|
||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
"no_results_description": "Try a synonym or more general keyword",
|
"no_results_description": "Try a synonym or more general keyword",
|
||||||
|
"no_notifications": "No notifications",
|
||||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||||
"not_in_any_album": "Not in any album",
|
"not_in_any_album": "Not in any album",
|
||||||
"not_selected": "Not selected",
|
"not_selected": "Not selected",
|
||||||
|
@ -1,25 +1,42 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.Result
|
||||||
|
import io.flutter.plugin.common.PluginRegistry
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android plugin for Dart `BackgroundService`
|
* Android plugin for Dart `BackgroundService` and file trash operations
|
||||||
*
|
|
||||||
* Receives messages/method calls from the foreground Dart side to manage
|
|
||||||
* the background service, e.g. start (enqueue), stop (cancel)
|
|
||||||
*/
|
*/
|
||||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
|
||||||
|
|
||||||
private var methodChannel: MethodChannel? = null
|
private var methodChannel: MethodChannel? = null
|
||||||
|
private var fileTrashChannel: MethodChannel? = null
|
||||||
private var context: Context? = null
|
private var context: Context? = null
|
||||||
|
private var pendingResult: Result? = null
|
||||||
|
private val permissionRequestCode = 1001
|
||||||
|
private val trashRequestCode = 1002
|
||||||
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||||
@ -29,6 +46,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
context = ctx
|
context = ctx
|
||||||
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
|
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
|
||||||
methodChannel?.setMethodCallHandler(this)
|
methodChannel?.setMethodCallHandler(this)
|
||||||
|
|
||||||
|
// Add file trash channel
|
||||||
|
fileTrashChannel = MethodChannel(messenger, "file_trash")
|
||||||
|
fileTrashChannel?.setMethodCallHandler(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
@ -38,11 +59,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
private fun onDetachedFromEngine() {
|
private fun onDetachedFromEngine() {
|
||||||
methodChannel?.setMethodCallHandler(null)
|
methodChannel?.setMethodCallHandler(null)
|
||||||
methodChannel = null
|
methodChannel = null
|
||||||
|
fileTrashChannel?.setMethodCallHandler(null)
|
||||||
|
fileTrashChannel = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||||
val ctx = context!!
|
val ctx = context!!
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
|
// Existing BackgroundService methods
|
||||||
"enable" -> {
|
"enable" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
@ -114,10 +138,184 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File Trash methods moved from MainActivity
|
||||||
|
"moveToTrash" -> {
|
||||||
|
val mediaUrls = call.argument<List<String>>("mediaUrls")
|
||||||
|
if (mediaUrls != null) {
|
||||||
|
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
|
||||||
|
moveToTrash(mediaUrls, result)
|
||||||
|
} else {
|
||||||
|
result.error("PERMISSION_DENIED", "Media permission required", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.error("INVALID_NAME", "The mediaUrls is not specified.", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"restoreFromTrash" -> {
|
||||||
|
val fileName = call.argument<String>("fileName")
|
||||||
|
val type = call.argument<Int>("type")
|
||||||
|
if (fileName != null && type != null) {
|
||||||
|
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
|
||||||
|
restoreFromTrash(fileName, type, result)
|
||||||
|
} else {
|
||||||
|
result.error("PERMISSION_DENIED", "Media permission required", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"requestManageMediaPermission" -> {
|
||||||
|
if (!hasManageMediaPermission()) {
|
||||||
|
requestManageMediaPermission(result)
|
||||||
|
} else {
|
||||||
|
Log.e("Manage storage permission", "Permission already granted")
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasManageMediaPermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
MediaStore.canManageMedia(context!!);
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestManageMediaPermission(result: Result) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
pendingResult = result // Store the result callback
|
||||||
|
val activity = activityBinding?.activity ?: return
|
||||||
|
|
||||||
|
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA)
|
||||||
|
intent.data = "package:${activity.packageName}".toUri()
|
||||||
|
activity.startActivityForResult(intent, permissionRequestCode)
|
||||||
|
} else {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun moveToTrash(mediaUrls: List<String>, result: Result) {
|
||||||
|
val urisToTrash = mediaUrls.map { it.toUri() }
|
||||||
|
if (urisToTrash.isEmpty()) {
|
||||||
|
result.error("INVALID_ARGS", "No valid URIs provided", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTrash(urisToTrash, true, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun restoreFromTrash(name: String, type: Int, result: Result) {
|
||||||
|
val uri = getTrashedFileUri(name, type)
|
||||||
|
if (uri == null) {
|
||||||
|
Log.e("TrashError", "Asset Uri cannot be found obtained")
|
||||||
|
result.error("TrashError", "Asset Uri cannot be found obtained", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.e("FILE_URI", uri.toString())
|
||||||
|
uri.let { toggleTrash(listOf(it), false, result) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun toggleTrash(contentUris: List<Uri>, isTrashed: Boolean, result: Result) {
|
||||||
|
val activity = activityBinding?.activity
|
||||||
|
val contentResolver = context?.contentResolver
|
||||||
|
if (activity == null || contentResolver == null) {
|
||||||
|
result.error("TrashError", "Activity or ContentResolver not available", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
|
||||||
|
pendingResult = result // Store for onActivityResult
|
||||||
|
activity.startIntentSenderForResult(
|
||||||
|
pendingIntent.intentSender,
|
||||||
|
trashRequestCode,
|
||||||
|
null, 0, 0, 0
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("TrashError", "Error creating or starting trash request", e)
|
||||||
|
result.error("TrashError", "Error creating or starting trash request", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun getTrashedFileUri(fileName: String, type: Int): Uri? {
|
||||||
|
val contentResolver = context?.contentResolver ?: return null
|
||||||
|
val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
|
||||||
|
|
||||||
|
val queryArgs = Bundle().apply {
|
||||||
|
putString(
|
||||||
|
ContentResolver.QUERY_ARG_SQL_SELECTION,
|
||||||
|
"${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
|
||||||
|
)
|
||||||
|
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
|
||||||
|
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
||||||
|
// same order as AssetType from dart
|
||||||
|
val contentUri = when (type) {
|
||||||
|
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
|
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||||
|
else -> queryUri
|
||||||
|
}
|
||||||
|
return ContentUris.withAppendedId(contentUri, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityAware implementation
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activityBinding = binding
|
||||||
|
binding.addActivityResultListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
activityBinding?.removeActivityResultListener(this)
|
||||||
|
activityBinding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
|
activityBinding = binding
|
||||||
|
binding.addActivityResultListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
activityBinding?.removeActivityResultListener(this)
|
||||||
|
activityBinding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityResultListener implementation
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (requestCode == permissionRequestCode) {
|
||||||
|
val granted = hasManageMediaPermission()
|
||||||
|
pendingResult?.success(granted)
|
||||||
|
pendingResult = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestCode == trashRequestCode) {
|
||||||
|
val approved = resultCode == Activity.RESULT_OK
|
||||||
|
pendingResult?.success(approved)
|
||||||
|
pendingResult = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val TAG = "BackgroundServicePlugin"
|
private const val TAG = "BackgroundServicePlugin"
|
||||||
private const val BUFFER_SIZE = 2 * 1024 * 1024;
|
private const val BUFFER_SIZE = 2 * 1024 * 1024
|
||||||
|
@ -2,14 +2,12 @@ package app.alextran.immich
|
|||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import android.os.Bundle
|
import androidx.annotation.NonNull
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||||
|
// No need to set up method channel here as it's now handled in the plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 195,
|
"android.injected.version.code" => 197,
|
||||||
"android.injected.version.name" => "1.132.1",
|
"android.injected.version.name" => "1.132.3",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
@ -261,9 +261,11 @@
|
|||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
CreatedOnToolsVersion = 7.3.1;
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
LastSwiftMigration = 1100;
|
LastSwiftMigration = 1100;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
FAC6F88F2D287C890078CB2F = {
|
FAC6F88F2D287C890078CB2F = {
|
||||||
CreatedOnToolsVersion = 16.0;
|
CreatedOnToolsVersion = 16.0;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -541,7 +543,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202;
|
CURRENT_PROJECT_VERSION = 205;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@ -685,7 +687,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202;
|
CURRENT_PROJECT_VERSION = 205;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@ -715,7 +717,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202;
|
CURRENT_PROJECT_VERSION = 205;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@ -748,7 +750,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202;
|
CURRENT_PROJECT_VERSION = 205;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -769,6 +771,7 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@ -791,7 +794,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202;
|
CURRENT_PROJECT_VERSION = 205;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -811,6 +814,7 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -831,7 +835,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202;
|
CURRENT_PROJECT_VERSION = 205;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -851,6 +855,7 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.132.0</string>
|
<string>1.132.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@ -93,7 +93,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>202</string>
|
<string>205</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
@ -18,8 +18,11 @@ default_platform(:ios)
|
|||||||
platform :ios do
|
platform :ios do
|
||||||
desc "iOS Release"
|
desc "iOS Release"
|
||||||
lane :release do
|
lane :release do
|
||||||
|
enable_automatic_code_signing(
|
||||||
|
path: "./Runner.xcodeproj",
|
||||||
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.132.1"
|
version_number: "1.132.3"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -65,6 +65,7 @@ enum StoreKey<T> {
|
|||||||
|
|
||||||
// Video settings
|
// Video settings
|
||||||
loadOriginalVideo<bool>._(136),
|
loadOriginalVideo<bool>._(136),
|
||||||
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000);
|
photoManagerCustomFilter<bool>._(1000);
|
||||||
|
5
mobile/lib/interfaces/local_files_manager.interface.dart
Normal file
5
mobile/lib/interfaces/local_files_manager.interface.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
abstract interface class ILocalFilesManager {
|
||||||
|
Future<bool> moveToTrash(List<String> mediaUrls);
|
||||||
|
Future<bool> restoreFromTrash(String fileName, int type);
|
||||||
|
Future<bool> requestManageMediaPermission();
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
@ -13,7 +12,6 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||||
@ -357,66 +355,51 @@ class PlacesCollectionCard extends StatelessWidget {
|
|||||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||||
final size = context.width * widthFactor - 20.0;
|
final size = context.width * widthFactor - 20.0;
|
||||||
|
|
||||||
return FutureBuilder<(Position?, LocationPermission?)>(
|
return GestureDetector(
|
||||||
future: MapUtils.checkPermAndGetLocation(
|
onTap: () => context.pushRoute(
|
||||||
context: context,
|
PlacesCollectionRoute(
|
||||||
silent: true,
|
currentLocation: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
child: Column(
|
||||||
var position = snapshot.data?.$1;
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
return GestureDetector(
|
children: [
|
||||||
onTap: () => context.pushRoute(
|
SizedBox(
|
||||||
PlacesCollectionRoute(
|
height: size,
|
||||||
currentLocation: position != null
|
width: size,
|
||||||
? LatLng(position.latitude, position.longitude)
|
child: DecoratedBox(
|
||||||
: null,
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
color:
|
||||||
|
context.colorScheme.secondaryContainer.withAlpha(100),
|
||||||
|
),
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: MapThumbnail(
|
||||||
|
zoom: 8,
|
||||||
|
centre: const LatLng(
|
||||||
|
21.44950,
|
||||||
|
-157.91959,
|
||||||
|
),
|
||||||
|
showAttribution: false,
|
||||||
|
themeMode: context.isDarkTheme
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(8.0),
|
||||||
children: [
|
child: Text(
|
||||||
SizedBox(
|
'places'.tr(),
|
||||||
height: size,
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
width: size,
|
color: context.colorScheme.onSurface,
|
||||||
child: DecoratedBox(
|
fontWeight: FontWeight.w500,
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(20)),
|
|
||||||
color: context.colorScheme.secondaryContainer
|
|
||||||
.withAlpha(100),
|
|
||||||
),
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: snapshot.connectionState ==
|
|
||||||
ConnectionState.waiting
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
|
||||||
: MapThumbnail(
|
|
||||||
zoom: 8,
|
|
||||||
centre: LatLng(
|
|
||||||
position?.latitude ?? 21.44950,
|
|
||||||
position?.longitude ?? -157.91959,
|
|
||||||
),
|
|
||||||
showAttribution: false,
|
|
||||||
themeMode: context.isDarkTheme
|
|
||||||
? ThemeMode.dark
|
|
||||||
: ThemeMode.light,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
'places'.tr(),
|
|
||||||
style: context.textTheme.titleSmall?.copyWith(
|
|
||||||
color: context.colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -44,7 +44,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'grant_permission',
|
'continue',
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -23,6 +23,7 @@ enum PendingAction {
|
|||||||
assetDelete,
|
assetDelete,
|
||||||
assetUploaded,
|
assetUploaded,
|
||||||
assetHidden,
|
assetHidden,
|
||||||
|
assetTrash,
|
||||||
}
|
}
|
||||||
|
|
||||||
class PendingChange {
|
class PendingChange {
|
||||||
@ -160,7 +161,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||||
socket.on('on_asset_trash', _handleServerUpdates);
|
socket.on('on_asset_trash', _handleOnAssetTrash);
|
||||||
socket.on('on_asset_restore', _handleServerUpdates);
|
socket.on('on_asset_restore', _handleServerUpdates);
|
||||||
socket.on('on_asset_update', _handleServerUpdates);
|
socket.on('on_asset_update', _handleServerUpdates);
|
||||||
socket.on('on_asset_stack_update', _handleServerUpdates);
|
socket.on('on_asset_stack_update', _handleServerUpdates);
|
||||||
@ -207,6 +208,26 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
_debounce.run(handlePendingChanges);
|
_debounce.run(handlePendingChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePendingTrashes() async {
|
||||||
|
final trashChanges = state.pendingChanges
|
||||||
|
.where((c) => c.action == PendingAction.assetTrash)
|
||||||
|
.toList();
|
||||||
|
if (trashChanges.isNotEmpty) {
|
||||||
|
List<String> remoteIds = trashChanges
|
||||||
|
.expand((a) => (a.value as List).map((e) => e.toString()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||||
|
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
pendingChanges: state.pendingChanges
|
||||||
|
.whereNot((c) => trashChanges.contains(c))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handlePendingDeletes() async {
|
Future<void> _handlePendingDeletes() async {
|
||||||
final deleteChanges = state.pendingChanges
|
final deleteChanges = state.pendingChanges
|
||||||
.where((c) => c.action == PendingAction.assetDelete)
|
.where((c) => c.action == PendingAction.assetDelete)
|
||||||
@ -267,6 +288,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
await _handlePendingUploaded();
|
await _handlePendingUploaded();
|
||||||
await _handlePendingDeletes();
|
await _handlePendingDeletes();
|
||||||
await _handlingPendingHidden();
|
await _handlingPendingHidden();
|
||||||
|
await _handlePendingTrashes();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnConfigUpdate(dynamic _) {
|
void _handleOnConfigUpdate(dynamic _) {
|
||||||
@ -285,6 +307,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
void _handleOnAssetDelete(dynamic data) =>
|
void _handleOnAssetDelete(dynamic data) =>
|
||||||
addPendingChange(PendingAction.assetDelete, data);
|
addPendingChange(PendingAction.assetDelete, data);
|
||||||
|
|
||||||
|
void _handleOnAssetTrash(dynamic data) {
|
||||||
|
addPendingChange(PendingAction.assetTrash, data);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleOnAssetHidden(dynamic data) =>
|
void _handleOnAssetHidden(dynamic data) =>
|
||||||
addPendingChange(PendingAction.assetHidden, data);
|
addPendingChange(PendingAction.assetHidden, data);
|
||||||
|
|
||||||
|
25
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
25
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||||
|
import 'package:immich_mobile/utils/local_files_manager.dart';
|
||||||
|
|
||||||
|
final localFilesManagerRepositoryProvider =
|
||||||
|
Provider((ref) => const LocalFilesManagerRepository());
|
||||||
|
|
||||||
|
class LocalFilesManagerRepository implements ILocalFilesManager {
|
||||||
|
const LocalFilesManagerRepository();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> moveToTrash(List<String> mediaUrls) async {
|
||||||
|
return await LocalFilesManager.moveToTrash(mediaUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> restoreFromTrash(String fileName, int type) async {
|
||||||
|
return await LocalFilesManager.restoreFromTrash(fileName, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> requestManageMediaPermission() async {
|
||||||
|
return await LocalFilesManager.requestManageMediaPermission();
|
||||||
|
}
|
||||||
|
}
|
@ -61,6 +61,7 @@ enum AppSettingsEnum<T> {
|
|||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||||
|
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -16,8 +17,10 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
|||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
@ -25,8 +28,10 @@ import 'package:immich_mobile/repositories/album_api.repository.dart';
|
|||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/partner.repository.dart';
|
import 'package:immich_mobile/repositories/partner.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/entity.service.dart';
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
@ -48,6 +53,8 @@ final syncServiceProvider = Provider(
|
|||||||
ref.watch(userRepositoryProvider),
|
ref.watch(userRepositoryProvider),
|
||||||
ref.watch(userServiceProvider),
|
ref.watch(userServiceProvider),
|
||||||
ref.watch(etagRepositoryProvider),
|
ref.watch(etagRepositoryProvider),
|
||||||
|
ref.watch(appSettingsServiceProvider),
|
||||||
|
ref.watch(localFilesManagerRepositoryProvider),
|
||||||
ref.watch(partnerApiRepositoryProvider),
|
ref.watch(partnerApiRepositoryProvider),
|
||||||
ref.watch(userApiRepositoryProvider),
|
ref.watch(userApiRepositoryProvider),
|
||||||
),
|
),
|
||||||
@ -69,6 +76,8 @@ class SyncService {
|
|||||||
final IUserApiRepository _userApiRepository;
|
final IUserApiRepository _userApiRepository;
|
||||||
final AsyncMutex _lock = AsyncMutex();
|
final AsyncMutex _lock = AsyncMutex();
|
||||||
final Logger _log = Logger('SyncService');
|
final Logger _log = Logger('SyncService');
|
||||||
|
final AppSettingsService _appSettingsService;
|
||||||
|
final ILocalFilesManager _localFilesManager;
|
||||||
|
|
||||||
SyncService(
|
SyncService(
|
||||||
this._hashService,
|
this._hashService,
|
||||||
@ -82,6 +91,8 @@ class SyncService {
|
|||||||
this._userRepository,
|
this._userRepository,
|
||||||
this._userService,
|
this._userService,
|
||||||
this._eTagRepository,
|
this._eTagRepository,
|
||||||
|
this._appSettingsService,
|
||||||
|
this._localFilesManager,
|
||||||
this._partnerApiRepository,
|
this._partnerApiRepository,
|
||||||
this._userApiRepository,
|
this._userApiRepository,
|
||||||
);
|
);
|
||||||
@ -238,8 +249,22 @@ class SyncService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _moveToTrashMatchedAssets(Iterable<String> idsToDelete) async {
|
||||||
|
final List<Asset> localAssets = await _assetRepository.getAllLocal();
|
||||||
|
final List<Asset> matchedAssets = localAssets
|
||||||
|
.where((asset) => idsToDelete.contains(asset.remoteId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final mediaUrls = await Future.wait(
|
||||||
|
matchedAssets
|
||||||
|
.map((asset) => asset.local?.getMediaUrl() ?? Future.value(null)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||||
|
}
|
||||||
|
|
||||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) async {
|
||||||
return _assetRepository.transaction(() async {
|
return _assetRepository.transaction(() async {
|
||||||
await _assetRepository.deleteAllByRemoteId(
|
await _assetRepository.deleteAllByRemoteId(
|
||||||
idsToDelete,
|
idsToDelete,
|
||||||
@ -249,6 +274,12 @@ class SyncService {
|
|||||||
idsToDelete,
|
idsToDelete,
|
||||||
state: AssetState.merged,
|
state: AssetState.merged,
|
||||||
);
|
);
|
||||||
|
if (Platform.isAndroid &&
|
||||||
|
_appSettingsService.getSetting<bool>(
|
||||||
|
AppSettingsEnum.manageLocalMediaAndroid,
|
||||||
|
)) {
|
||||||
|
await _moveToTrashMatchedAssets(idsToDelete);
|
||||||
|
}
|
||||||
if (merged.isEmpty) return;
|
if (merged.isEmpty) return;
|
||||||
for (final Asset asset in merged) {
|
for (final Asset asset in merged) {
|
||||||
asset.remoteId = null;
|
asset.remoteId = null;
|
||||||
@ -790,10 +821,43 @@ class SyncService {
|
|||||||
return (existing, toUpsert);
|
return (existing, toUpsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleTrashStatusForAssets(List<Asset> assetsList) async {
|
||||||
|
final trashMediaUrls = <String>[];
|
||||||
|
|
||||||
|
for (final asset in assetsList) {
|
||||||
|
if (asset.isTrashed) {
|
||||||
|
final mediaUrl = await asset.local?.getMediaUrl();
|
||||||
|
if (mediaUrl == null) {
|
||||||
|
_log.warning(
|
||||||
|
"Failed to get media URL for asset ${asset.name} while moving to trash",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trashMediaUrls.add(mediaUrl);
|
||||||
|
} else {
|
||||||
|
await _localFilesManager.restoreFromTrash(
|
||||||
|
asset.fileName,
|
||||||
|
asset.type.index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trashMediaUrls.isNotEmpty) {
|
||||||
|
await _localFilesManager.moveToTrash(trashMediaUrls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||||
if (assets.isEmpty) return;
|
if (assets.isEmpty) return;
|
||||||
|
|
||||||
|
if (Platform.isAndroid &&
|
||||||
|
_appSettingsService.getSetting<bool>(
|
||||||
|
AppSettingsEnum.manageLocalMediaAndroid,
|
||||||
|
)) {
|
||||||
|
_toggleTrashStatusForAssets(assets);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _assetRepository.transaction(() async {
|
await _assetRepository.transaction(() async {
|
||||||
await _assetRepository.updateAll(assets);
|
await _assetRepository.updateAll(assets);
|
||||||
|
38
mobile/lib/utils/local_files_manager.dart
Normal file
38
mobile/lib/utils/local_files_manager.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
abstract final class LocalFilesManager {
|
||||||
|
static final Logger _logger = Logger('LocalFilesManager');
|
||||||
|
static const MethodChannel _channel = MethodChannel('file_trash');
|
||||||
|
|
||||||
|
static Future<bool> moveToTrash(List<String> mediaUrls) async {
|
||||||
|
try {
|
||||||
|
return await _channel
|
||||||
|
.invokeMethod('moveToTrash', {'mediaUrls': mediaUrls});
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.warning('Error moving file to trash', e, s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> restoreFromTrash(String fileName, int type) async {
|
||||||
|
try {
|
||||||
|
return await _channel.invokeMethod(
|
||||||
|
'restoreFromTrash',
|
||||||
|
{'fileName': fileName, 'type': type},
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.warning('Error restore file from trash', e, s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> requestManageMediaPermission() async {
|
||||||
|
try {
|
||||||
|
return await _channel.invokeMethod('requestManageMediaPermission');
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.warning('Error requesting manage media permission', e, s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||||
@ -17,6 +17,8 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
// ignore: import_rule_photo_manager
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
const int targetVersion = 10;
|
const int targetVersion = 10;
|
||||||
|
|
||||||
@ -69,14 +71,45 @@ Future<void> _migrateDeviceAsset(Isar db) async {
|
|||||||
: (await db.iOSDeviceAssets.where().findAll())
|
: (await db.iOSDeviceAssets.where().findAll())
|
||||||
.map((i) => _DeviceAsset(assetId: i.id, hash: i.hash))
|
.map((i) => _DeviceAsset(assetId: i.id, hash: i.hash))
|
||||||
.toList();
|
.toList();
|
||||||
final localAssets = (await db.assets
|
|
||||||
.where()
|
final PermissionState ps = await PhotoManager.requestPermissionExtend();
|
||||||
.anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId))
|
if (!ps.hasAccess) {
|
||||||
.findAll())
|
if (kDebugMode) {
|
||||||
.map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt))
|
debugPrint(
|
||||||
.toList();
|
"[MIGRATION] Photo library permission not granted. Skipping device asset migration.",
|
||||||
debugPrint("Device Asset Ids length - ${ids.length}");
|
);
|
||||||
debugPrint("Local Asset Ids length - ${localAssets.length}");
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_DeviceAsset> localAssets = [];
|
||||||
|
final List<AssetPathEntity> paths =
|
||||||
|
await PhotoManager.getAssetPathList(onlyAll: true);
|
||||||
|
|
||||||
|
if (paths.isEmpty) {
|
||||||
|
localAssets = (await db.assets
|
||||||
|
.where()
|
||||||
|
.anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId))
|
||||||
|
.findAll())
|
||||||
|
.map(
|
||||||
|
(a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
final AssetPathEntity albumWithAll = paths.first;
|
||||||
|
final int assetCount = await albumWithAll.assetCountAsync;
|
||||||
|
|
||||||
|
final List<AssetEntity> allDeviceAssets =
|
||||||
|
await albumWithAll.getAssetListRange(start: 0, end: assetCount);
|
||||||
|
|
||||||
|
localAssets = allDeviceAssets
|
||||||
|
.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}");
|
||||||
|
debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}");
|
||||||
ids.sort((a, b) => a.assetId.compareTo(b.assetId));
|
ids.sort((a, b) => a.assetId.compareTo(b.assetId));
|
||||||
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
|
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
|
||||||
final List<DeviceAssetEntity> toAdd = [];
|
final List<DeviceAssetEntity> toAdd = [];
|
||||||
@ -95,15 +128,27 @@ Future<void> _migrateDeviceAsset(Isar db) async {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onlyFirst: (deviceAsset) {
|
onlyFirst: (deviceAsset) {
|
||||||
debugPrint(
|
if (kDebugMode) {
|
||||||
'DeviceAsset not found in local assets: ${deviceAsset.assetId}',
|
debugPrint(
|
||||||
);
|
'[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}',
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onlySecond: (asset) {
|
onlySecond: (asset) {
|
||||||
debugPrint('Local asset not found in DeviceAsset: ${asset.assetId}');
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}',
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
debugPrint("Total number of device assets migrated - ${toAdd.length}");
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
"[MIGRATION] Total number of device assets migrated - ${toAdd.length}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await db.writeTxn(() async {
|
await db.writeTxn(() async {
|
||||||
await db.deviceAssetEntitys.putAll(toAdd);
|
await db.deviceAssetEntitys.putAll(toAdd);
|
||||||
});
|
});
|
||||||
|
@ -97,6 +97,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (7 - scaleFactor.value.toInt() != perRow.value) {
|
if (7 - scaleFactor.value.toInt() != perRow.value) {
|
||||||
perRow.value = 7 - scaleFactor.value.toInt();
|
perRow.value = 7 - scaleFactor.value.toInt();
|
||||||
|
settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -755,7 +755,7 @@ class _MonthTitle extends StatelessWidget {
|
|||||||
key: Key("month-$title"),
|
key: Key("month-$title"),
|
||||||
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
|
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
toBeginningOfSentenceCase(title, context.locale.languageCode),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@ -786,7 +786,7 @@ class _Title extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GroupDividerTitle(
|
return GroupDividerTitle(
|
||||||
text: title,
|
text: toBeginningOfSentenceCase(title, context.locale.languageCode),
|
||||||
multiselectEnabled: selectionActive,
|
multiselectEnabled: selectionActive,
|
||||||
onSelect: () => selectAssets(assets),
|
onSelect: () => selectAssets(assets),
|
||||||
onDeselect: () => deselectAssets(assets),
|
onDeselect: () => deselectAssets(assets),
|
||||||
|
@ -207,9 +207,27 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String generateRandomString(int length) {
|
String generateRandomString(int length) {
|
||||||
|
const chars =
|
||||||
|
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||||
final random = Random.secure();
|
final random = Random.secure();
|
||||||
return base64Url
|
return String.fromCharCodes(
|
||||||
.encode(List<int>.generate(32, (i) => random.nextInt(256)));
|
Iterable.generate(
|
||||||
|
length,
|
||||||
|
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> randomBytes(int length) {
|
||||||
|
final random = Random.secure();
|
||||||
|
return List<int>.generate(length, (i) => random.nextInt(256));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per specification, the code verifier must be 43-128 characters long
|
||||||
|
/// and consist of characters [A-Z, a-z, 0-9, "-", ".", "_", "~"]
|
||||||
|
/// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
|
||||||
|
String randomCodeVerifier() {
|
||||||
|
return base64Url.encode(randomBytes(42));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> generatePKCECodeChallenge(String codeVerifier) async {
|
Future<String> generatePKCECodeChallenge(String codeVerifier) async {
|
||||||
@ -223,7 +241,8 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
String? oAuthServerUrl;
|
String? oAuthServerUrl;
|
||||||
|
|
||||||
final state = generateRandomString(32);
|
final state = generateRandomString(32);
|
||||||
final codeVerifier = generateRandomString(64);
|
|
||||||
|
final codeVerifier = randomCodeVerifier();
|
||||||
final codeChallenge = await generatePKCECodeChallenge(codeVerifier);
|
final codeChallenge = await generatePKCECodeChallenge(codeVerifier);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
@ -25,6 +27,8 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
final advancedTroubleshooting =
|
final advancedTroubleshooting =
|
||||||
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
|
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
|
||||||
|
final manageLocalMediaAndroid =
|
||||||
|
useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||||
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
||||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||||
final allowSelfSignedSSLCert =
|
final allowSelfSignedSSLCert =
|
||||||
@ -40,6 +44,16 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<bool> checkAndroidVersion() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||||
|
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||||
|
int sdkVersion = androidInfo.version.sdkInt;
|
||||||
|
return sdkVersion >= 31;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final advancedSettings = [
|
final advancedSettings = [
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -47,6 +61,29 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
title: "advanced_settings_troubleshooting_title".tr(),
|
title: "advanced_settings_troubleshooting_title".tr(),
|
||||||
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
||||||
),
|
),
|
||||||
|
FutureBuilder<bool>(
|
||||||
|
future: checkAndroidVersion(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData && snapshot.data == true) {
|
||||||
|
return SettingsSwitchListTile(
|
||||||
|
enabled: true,
|
||||||
|
valueNotifier: manageLocalMediaAndroid,
|
||||||
|
title: "advanced_settings_sync_remote_deletions_title".tr(),
|
||||||
|
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value) {
|
||||||
|
final result = await ref
|
||||||
|
.read(localFilesManagerRepositoryProvider)
|
||||||
|
.requestManageMediaPermission();
|
||||||
|
manageLocalMediaAndroid.value = result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
SettingsSliderListTile(
|
SettingsSliderListTile(
|
||||||
text: "advanced_settings_log_level_title".tr(args: [logLevel]),
|
text: "advanced_settings_log_level_title".tr(args: [logLevel]),
|
||||||
valueNotifier: levelId,
|
valueNotifier: levelId,
|
||||||
|
21
mobile/openapi/README.md
generated
21
mobile/openapi/README.md
generated
@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.132.1
|
- API version: 1.132.3
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
@ -145,8 +145,15 @@ Class | Method | HTTP request | Description
|
|||||||
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
||||||
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
||||||
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
||||||
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} |
|
*NotificationsApi* | [**deleteNotification**](doc//NotificationsApi.md#deletenotification) | **DELETE** /notifications/{id} |
|
||||||
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email |
|
*NotificationsApi* | [**deleteNotifications**](doc//NotificationsApi.md#deletenotifications) | **DELETE** /notifications |
|
||||||
|
*NotificationsApi* | [**getNotification**](doc//NotificationsApi.md#getnotification) | **GET** /notifications/{id} |
|
||||||
|
*NotificationsApi* | [**getNotifications**](doc//NotificationsApi.md#getnotifications) | **GET** /notifications |
|
||||||
|
*NotificationsApi* | [**updateNotification**](doc//NotificationsApi.md#updatenotification) | **PUT** /notifications/{id} |
|
||||||
|
*NotificationsApi* | [**updateNotifications**](doc//NotificationsApi.md#updatenotifications) | **PUT** /notifications |
|
||||||
|
*NotificationsAdminApi* | [**createNotification**](doc//NotificationsAdminApi.md#createnotification) | **POST** /admin/notifications |
|
||||||
|
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /admin/notifications/templates/{name} |
|
||||||
|
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /admin/notifications/test-email |
|
||||||
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
||||||
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
||||||
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
||||||
@ -300,7 +307,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
||||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||||
- [AudioCodec](doc//AudioCodec.md)
|
- [AudioCodec](doc//AudioCodec.md)
|
||||||
- [AvatarResponse](doc//AvatarResponse.md)
|
|
||||||
- [AvatarUpdate](doc//AvatarUpdate.md)
|
- [AvatarUpdate](doc//AvatarUpdate.md)
|
||||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||||
@ -361,6 +367,13 @@ Class | Method | HTTP request | Description
|
|||||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||||
- [MergePersonDto](doc//MergePersonDto.md)
|
- [MergePersonDto](doc//MergePersonDto.md)
|
||||||
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
||||||
|
- [NotificationCreateDto](doc//NotificationCreateDto.md)
|
||||||
|
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
|
||||||
|
- [NotificationDto](doc//NotificationDto.md)
|
||||||
|
- [NotificationLevel](doc//NotificationLevel.md)
|
||||||
|
- [NotificationType](doc//NotificationType.md)
|
||||||
|
- [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md)
|
||||||
|
- [NotificationUpdateDto](doc//NotificationUpdateDto.md)
|
||||||
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
|
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
|
||||||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||||
|
9
mobile/openapi/lib/api.dart
generated
9
mobile/openapi/lib/api.dart
generated
@ -44,6 +44,7 @@ part 'api/jobs_api.dart';
|
|||||||
part 'api/libraries_api.dart';
|
part 'api/libraries_api.dart';
|
||||||
part 'api/map_api.dart';
|
part 'api/map_api.dart';
|
||||||
part 'api/memories_api.dart';
|
part 'api/memories_api.dart';
|
||||||
|
part 'api/notifications_api.dart';
|
||||||
part 'api/notifications_admin_api.dart';
|
part 'api/notifications_admin_api.dart';
|
||||||
part 'api/o_auth_api.dart';
|
part 'api/o_auth_api.dart';
|
||||||
part 'api/partners_api.dart';
|
part 'api/partners_api.dart';
|
||||||
@ -107,7 +108,6 @@ part 'model/asset_stack_response_dto.dart';
|
|||||||
part 'model/asset_stats_response_dto.dart';
|
part 'model/asset_stats_response_dto.dart';
|
||||||
part 'model/asset_type_enum.dart';
|
part 'model/asset_type_enum.dart';
|
||||||
part 'model/audio_codec.dart';
|
part 'model/audio_codec.dart';
|
||||||
part 'model/avatar_response.dart';
|
|
||||||
part 'model/avatar_update.dart';
|
part 'model/avatar_update.dart';
|
||||||
part 'model/bulk_id_response_dto.dart';
|
part 'model/bulk_id_response_dto.dart';
|
||||||
part 'model/bulk_ids_dto.dart';
|
part 'model/bulk_ids_dto.dart';
|
||||||
@ -168,6 +168,13 @@ part 'model/memory_type.dart';
|
|||||||
part 'model/memory_update_dto.dart';
|
part 'model/memory_update_dto.dart';
|
||||||
part 'model/merge_person_dto.dart';
|
part 'model/merge_person_dto.dart';
|
||||||
part 'model/metadata_search_dto.dart';
|
part 'model/metadata_search_dto.dart';
|
||||||
|
part 'model/notification_create_dto.dart';
|
||||||
|
part 'model/notification_delete_all_dto.dart';
|
||||||
|
part 'model/notification_dto.dart';
|
||||||
|
part 'model/notification_level.dart';
|
||||||
|
part 'model/notification_type.dart';
|
||||||
|
part 'model/notification_update_all_dto.dart';
|
||||||
|
part 'model/notification_update_dto.dart';
|
||||||
part 'model/o_auth_authorize_response_dto.dart';
|
part 'model/o_auth_authorize_response_dto.dart';
|
||||||
part 'model/o_auth_callback_dto.dart';
|
part 'model/o_auth_callback_dto.dart';
|
||||||
part 'model/o_auth_config_dto.dart';
|
part 'model/o_auth_config_dto.dart';
|
||||||
|
55
mobile/openapi/lib/api/notifications_admin_api.dart
generated
55
mobile/openapi/lib/api/notifications_admin_api.dart
generated
@ -16,7 +16,54 @@ class NotificationsAdminApi {
|
|||||||
|
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /notifications/admin/templates/{name}' operation and returns the [Response].
|
/// Performs an HTTP 'POST /admin/notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationCreateDto] notificationCreateDto (required):
|
||||||
|
Future<Response> createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationCreateDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationCreateDto] notificationCreateDto (required):
|
||||||
|
Future<NotificationDto?> createNotification(NotificationCreateDto notificationCreateDto,) async {
|
||||||
|
final response = await createNotificationWithHttpInfo(notificationCreateDto,);
|
||||||
|
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), 'NotificationDto',) as NotificationDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /admin/notifications/templates/{name}' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] name (required):
|
/// * [String] name (required):
|
||||||
@ -24,7 +71,7 @@ class NotificationsAdminApi {
|
|||||||
/// * [TemplateDto] templateDto (required):
|
/// * [TemplateDto] templateDto (required):
|
||||||
Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async {
|
Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/notifications/admin/templates/{name}'
|
final apiPath = r'/admin/notifications/templates/{name}'
|
||||||
.replaceAll('{name}', name);
|
.replaceAll('{name}', name);
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
// ignore: prefer_final_locals
|
||||||
@ -68,13 +115,13 @@ class NotificationsAdminApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /notifications/admin/test-email' operation and returns the [Response].
|
/// Performs an HTTP 'POST /admin/notifications/test-email' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
||||||
Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/notifications/admin/test-email';
|
final apiPath = r'/admin/notifications/test-email';
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
// ignore: prefer_final_locals
|
||||||
Object? postBody = systemConfigSmtpDto;
|
Object? postBody = systemConfigSmtpDto;
|
||||||
|
311
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
311
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationsApi {
|
||||||
|
NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /notifications/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<Response> deleteNotificationWithHttpInfo(String id,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<void> deleteNotification(String id,) async {
|
||||||
|
final response = await deleteNotificationWithHttpInfo(id,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
|
||||||
|
Future<Response> deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationDeleteAllDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
|
||||||
|
Future<void> deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async {
|
||||||
|
final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /notifications/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<Response> getNotificationWithHttpInfo(String id,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<NotificationDto?> getNotification(String id,) async {
|
||||||
|
final response = await getNotificationWithHttpInfo(id,);
|
||||||
|
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), 'NotificationDto',) as NotificationDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id:
|
||||||
|
///
|
||||||
|
/// * [NotificationLevel] level:
|
||||||
|
///
|
||||||
|
/// * [NotificationType] type:
|
||||||
|
///
|
||||||
|
/// * [bool] unread:
|
||||||
|
Future<Response> getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (id != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'id', id));
|
||||||
|
}
|
||||||
|
if (level != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'level', level));
|
||||||
|
}
|
||||||
|
if (type != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'type', type));
|
||||||
|
}
|
||||||
|
if (unread != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'unread', unread));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id:
|
||||||
|
///
|
||||||
|
/// * [NotificationLevel] level:
|
||||||
|
///
|
||||||
|
/// * [NotificationType] type:
|
||||||
|
///
|
||||||
|
/// * [bool] unread:
|
||||||
|
Future<List<NotificationDto>?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||||
|
final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, );
|
||||||
|
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) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<NotificationDto>') as List)
|
||||||
|
.cast<NotificationDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /notifications/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateDto] notificationUpdateDto (required):
|
||||||
|
Future<Response> updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationUpdateDto;
|
||||||
|
|
||||||
|
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):
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateDto] notificationUpdateDto (required):
|
||||||
|
Future<NotificationDto?> updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async {
|
||||||
|
final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,);
|
||||||
|
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), 'NotificationDto',) as NotificationDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
|
||||||
|
Future<Response> updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationUpdateAllDto;
|
||||||
|
|
||||||
|
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:
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
|
||||||
|
Future<void> updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async {
|
||||||
|
final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
mobile/openapi/lib/api_client.dart
generated
16
mobile/openapi/lib/api_client.dart
generated
@ -270,8 +270,6 @@ class ApiClient {
|
|||||||
return AssetTypeEnumTypeTransformer().decode(value);
|
return AssetTypeEnumTypeTransformer().decode(value);
|
||||||
case 'AudioCodec':
|
case 'AudioCodec':
|
||||||
return AudioCodecTypeTransformer().decode(value);
|
return AudioCodecTypeTransformer().decode(value);
|
||||||
case 'AvatarResponse':
|
|
||||||
return AvatarResponse.fromJson(value);
|
|
||||||
case 'AvatarUpdate':
|
case 'AvatarUpdate':
|
||||||
return AvatarUpdate.fromJson(value);
|
return AvatarUpdate.fromJson(value);
|
||||||
case 'BulkIdResponseDto':
|
case 'BulkIdResponseDto':
|
||||||
@ -392,6 +390,20 @@ class ApiClient {
|
|||||||
return MergePersonDto.fromJson(value);
|
return MergePersonDto.fromJson(value);
|
||||||
case 'MetadataSearchDto':
|
case 'MetadataSearchDto':
|
||||||
return MetadataSearchDto.fromJson(value);
|
return MetadataSearchDto.fromJson(value);
|
||||||
|
case 'NotificationCreateDto':
|
||||||
|
return NotificationCreateDto.fromJson(value);
|
||||||
|
case 'NotificationDeleteAllDto':
|
||||||
|
return NotificationDeleteAllDto.fromJson(value);
|
||||||
|
case 'NotificationDto':
|
||||||
|
return NotificationDto.fromJson(value);
|
||||||
|
case 'NotificationLevel':
|
||||||
|
return NotificationLevelTypeTransformer().decode(value);
|
||||||
|
case 'NotificationType':
|
||||||
|
return NotificationTypeTypeTransformer().decode(value);
|
||||||
|
case 'NotificationUpdateAllDto':
|
||||||
|
return NotificationUpdateAllDto.fromJson(value);
|
||||||
|
case 'NotificationUpdateDto':
|
||||||
|
return NotificationUpdateDto.fromJson(value);
|
||||||
case 'OAuthAuthorizeResponseDto':
|
case 'OAuthAuthorizeResponseDto':
|
||||||
return OAuthAuthorizeResponseDto.fromJson(value);
|
return OAuthAuthorizeResponseDto.fromJson(value);
|
||||||
case 'OAuthCallbackDto':
|
case 'OAuthCallbackDto':
|
||||||
|
6
mobile/openapi/lib/api_helper.dart
generated
6
mobile/openapi/lib/api_helper.dart
generated
@ -100,6 +100,12 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is MemoryType) {
|
if (value is MemoryType) {
|
||||||
return MemoryTypeTypeTransformer().encode(value).toString();
|
return MemoryTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
if (value is NotificationLevel) {
|
||||||
|
return NotificationLevelTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
|
if (value is NotificationType) {
|
||||||
|
return NotificationTypeTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
if (value is PartnerDirection) {
|
if (value is PartnerDirection) {
|
||||||
return PartnerDirectionTypeTransformer().encode(value).toString();
|
return PartnerDirectionTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
180
mobile/openapi/lib/model/notification_create_dto.dart
generated
Normal file
180
mobile/openapi/lib/model/notification_create_dto.dart
generated
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationCreateDto {
|
||||||
|
/// Returns a new [NotificationCreateDto] instance.
|
||||||
|
NotificationCreateDto({
|
||||||
|
this.data,
|
||||||
|
this.description,
|
||||||
|
this.level,
|
||||||
|
this.readAt,
|
||||||
|
required this.title,
|
||||||
|
this.type,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
Object? data;
|
||||||
|
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
NotificationLevel? level;
|
||||||
|
|
||||||
|
DateTime? readAt;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
NotificationType? type;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto &&
|
||||||
|
other.data == data &&
|
||||||
|
other.description == description &&
|
||||||
|
other.level == level &&
|
||||||
|
other.readAt == readAt &&
|
||||||
|
other.title == title &&
|
||||||
|
other.type == type &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(data == null ? 0 : data!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
|
(level == null ? 0 : level!.hashCode) +
|
||||||
|
(readAt == null ? 0 : readAt!.hashCode) +
|
||||||
|
(title.hashCode) +
|
||||||
|
(type == null ? 0 : type!.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationCreateDto[data=$data, description=$description, level=$level, readAt=$readAt, title=$title, type=$type, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (this.data != null) {
|
||||||
|
json[r'data'] = this.data;
|
||||||
|
} else {
|
||||||
|
// json[r'data'] = null;
|
||||||
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
|
if (this.level != null) {
|
||||||
|
json[r'level'] = this.level;
|
||||||
|
} else {
|
||||||
|
// json[r'level'] = null;
|
||||||
|
}
|
||||||
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
|
json[r'title'] = this.title;
|
||||||
|
if (this.type != null) {
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
} else {
|
||||||
|
// json[r'type'] = null;
|
||||||
|
}
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationCreateDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationCreateDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationCreateDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationCreateDto(
|
||||||
|
data: mapValueOfType<Object>(json, r'data'),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
|
level: NotificationLevel.fromJson(json[r'level']),
|
||||||
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
|
title: mapValueOfType<String>(json, r'title')!,
|
||||||
|
type: NotificationType.fromJson(json[r'type']),
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationCreateDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationCreateDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationCreateDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationCreateDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationCreateDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationCreateDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationCreateDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationCreateDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'title',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
101
mobile/openapi/lib/model/notification_delete_all_dto.dart
generated
Normal file
101
mobile/openapi/lib/model/notification_delete_all_dto.dart
generated
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationDeleteAllDto {
|
||||||
|
/// Returns a new [NotificationDeleteAllDto] instance.
|
||||||
|
NotificationDeleteAllDto({
|
||||||
|
this.ids = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> ids;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationDeleteAllDto &&
|
||||||
|
_deepEquality.equals(other.ids, ids);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(ids.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationDeleteAllDto[ids=$ids]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'ids'] = this.ids;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationDeleteAllDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationDeleteAllDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationDeleteAllDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationDeleteAllDto(
|
||||||
|
ids: json[r'ids'] is Iterable
|
||||||
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationDeleteAllDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationDeleteAllDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationDeleteAllDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationDeleteAllDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationDeleteAllDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationDeleteAllDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationDeleteAllDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationDeleteAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationDeleteAllDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationDeleteAllDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'ids',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
182
mobile/openapi/lib/model/notification_dto.dart
generated
Normal file
182
mobile/openapi/lib/model/notification_dto.dart
generated
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationDto {
|
||||||
|
/// Returns a new [NotificationDto] instance.
|
||||||
|
NotificationDto({
|
||||||
|
required this.createdAt,
|
||||||
|
this.data,
|
||||||
|
this.description,
|
||||||
|
required this.id,
|
||||||
|
required this.level,
|
||||||
|
this.readAt,
|
||||||
|
required this.title,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
Object? data;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
NotificationLevel level;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
DateTime? readAt;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
NotificationType type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationDto &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.data == data &&
|
||||||
|
other.description == description &&
|
||||||
|
other.id == id &&
|
||||||
|
other.level == level &&
|
||||||
|
other.readAt == readAt &&
|
||||||
|
other.title == title &&
|
||||||
|
other.type == type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(data == null ? 0 : data!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(level.hashCode) +
|
||||||
|
(readAt == null ? 0 : readAt!.hashCode) +
|
||||||
|
(title.hashCode) +
|
||||||
|
(type.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationDto[createdAt=$createdAt, data=$data, description=$description, id=$id, level=$level, readAt=$readAt, title=$title, type=$type]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
if (this.data != null) {
|
||||||
|
json[r'data'] = this.data;
|
||||||
|
} else {
|
||||||
|
// json[r'data'] = null;
|
||||||
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'level'] = this.level;
|
||||||
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
|
json[r'title'] = this.title;
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationDto(
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
data: mapValueOfType<Object>(json, r'data'),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
level: NotificationLevel.fromJson(json[r'level'])!,
|
||||||
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
|
title: mapValueOfType<String>(json, r'title')!,
|
||||||
|
type: NotificationType.fromJson(json[r'type'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'createdAt',
|
||||||
|
'id',
|
||||||
|
'level',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
91
mobile/openapi/lib/model/notification_level.dart
generated
Normal file
91
mobile/openapi/lib/model/notification_level.dart
generated
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationLevel {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const NotificationLevel._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const success = NotificationLevel._(r'success');
|
||||||
|
static const error = NotificationLevel._(r'error');
|
||||||
|
static const warning = NotificationLevel._(r'warning');
|
||||||
|
static const info = NotificationLevel._(r'info');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][NotificationLevel].
|
||||||
|
static const values = <NotificationLevel>[
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
];
|
||||||
|
|
||||||
|
static NotificationLevel? fromJson(dynamic value) => NotificationLevelTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<NotificationLevel> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationLevel>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationLevel.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [NotificationLevel] to String,
|
||||||
|
/// and [decode] dynamic data back to [NotificationLevel].
|
||||||
|
class NotificationLevelTypeTransformer {
|
||||||
|
factory NotificationLevelTypeTransformer() => _instance ??= const NotificationLevelTypeTransformer._();
|
||||||
|
|
||||||
|
const NotificationLevelTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(NotificationLevel data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a NotificationLevel.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
NotificationLevel? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'success': return NotificationLevel.success;
|
||||||
|
case r'error': return NotificationLevel.error;
|
||||||
|
case r'warning': return NotificationLevel.warning;
|
||||||
|
case r'info': return NotificationLevel.info;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [NotificationLevelTypeTransformer] instance.
|
||||||
|
static NotificationLevelTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
91
mobile/openapi/lib/model/notification_type.dart
generated
Normal file
91
mobile/openapi/lib/model/notification_type.dart
generated
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationType {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const NotificationType._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const jobFailed = NotificationType._(r'JobFailed');
|
||||||
|
static const backupFailed = NotificationType._(r'BackupFailed');
|
||||||
|
static const systemMessage = NotificationType._(r'SystemMessage');
|
||||||
|
static const custom = NotificationType._(r'Custom');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][NotificationType].
|
||||||
|
static const values = <NotificationType>[
|
||||||
|
jobFailed,
|
||||||
|
backupFailed,
|
||||||
|
systemMessage,
|
||||||
|
custom,
|
||||||
|
];
|
||||||
|
|
||||||
|
static NotificationType? fromJson(dynamic value) => NotificationTypeTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<NotificationType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationType>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationType.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [NotificationType] to String,
|
||||||
|
/// and [decode] dynamic data back to [NotificationType].
|
||||||
|
class NotificationTypeTypeTransformer {
|
||||||
|
factory NotificationTypeTypeTransformer() => _instance ??= const NotificationTypeTypeTransformer._();
|
||||||
|
|
||||||
|
const NotificationTypeTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(NotificationType data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a NotificationType.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
NotificationType? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'JobFailed': return NotificationType.jobFailed;
|
||||||
|
case r'BackupFailed': return NotificationType.backupFailed;
|
||||||
|
case r'SystemMessage': return NotificationType.systemMessage;
|
||||||
|
case r'Custom': return NotificationType.custom;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [NotificationTypeTypeTransformer] instance.
|
||||||
|
static NotificationTypeTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
112
mobile/openapi/lib/model/notification_update_all_dto.dart
generated
Normal file
112
mobile/openapi/lib/model/notification_update_all_dto.dart
generated
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// 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 NotificationUpdateAllDto {
|
||||||
|
/// Returns a new [NotificationUpdateAllDto] instance.
|
||||||
|
NotificationUpdateAllDto({
|
||||||
|
this.ids = const [],
|
||||||
|
this.readAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> ids;
|
||||||
|
|
||||||
|
DateTime? readAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateAllDto &&
|
||||||
|
_deepEquality.equals(other.ids, ids) &&
|
||||||
|
other.readAt == readAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(ids.hashCode) +
|
||||||
|
(readAt == null ? 0 : readAt!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationUpdateAllDto[ids=$ids, readAt=$readAt]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'ids'] = this.ids;
|
||||||
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationUpdateAllDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationUpdateAllDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationUpdateAllDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationUpdateAllDto(
|
||||||
|
ids: json[r'ids'] is Iterable
|
||||||
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationUpdateAllDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationUpdateAllDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationUpdateAllDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationUpdateAllDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationUpdateAllDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationUpdateAllDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationUpdateAllDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationUpdateAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationUpdateAllDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationUpdateAllDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'ids',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,52 +10,56 @@
|
|||||||
|
|
||||||
part of openapi.api;
|
part of openapi.api;
|
||||||
|
|
||||||
class AvatarResponse {
|
class NotificationUpdateDto {
|
||||||
/// Returns a new [AvatarResponse] instance.
|
/// Returns a new [NotificationUpdateDto] instance.
|
||||||
AvatarResponse({
|
NotificationUpdateDto({
|
||||||
required this.color,
|
this.readAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
UserAvatarColor color;
|
DateTime? readAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AvatarResponse &&
|
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateDto &&
|
||||||
other.color == color;
|
other.readAt == readAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(color.hashCode);
|
(readAt == null ? 0 : readAt!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AvatarResponse[color=$color]';
|
String toString() => 'NotificationUpdateDto[readAt=$readAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'color'] = this.color;
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [AvatarResponse] instance and imports its values from
|
/// Returns a new [NotificationUpdateDto] instance and imports its values from
|
||||||
/// [value] if it's a [Map], null otherwise.
|
/// [value] if it's a [Map], null otherwise.
|
||||||
// ignore: prefer_constructors_over_static_methods
|
// ignore: prefer_constructors_over_static_methods
|
||||||
static AvatarResponse? fromJson(dynamic value) {
|
static NotificationUpdateDto? fromJson(dynamic value) {
|
||||||
upgradeDto(value, "AvatarResponse");
|
upgradeDto(value, "NotificationUpdateDto");
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AvatarResponse(
|
return NotificationUpdateDto(
|
||||||
color: UserAvatarColor.fromJson(json[r'color'])!,
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AvatarResponse> listFromJson(dynamic json, {bool growable = false,}) {
|
static List<NotificationUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final result = <AvatarResponse>[];
|
final result = <NotificationUpdateDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
final value = AvatarResponse.fromJson(row);
|
final value = NotificationUpdateDto.fromJson(row);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
result.add(value);
|
result.add(value);
|
||||||
}
|
}
|
||||||
@ -64,12 +68,12 @@ class AvatarResponse {
|
|||||||
return result.toList(growable: growable);
|
return result.toList(growable: growable);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, AvatarResponse> mapFromJson(dynamic json) {
|
static Map<String, NotificationUpdateDto> mapFromJson(dynamic json) {
|
||||||
final map = <String, AvatarResponse>{};
|
final map = <String, NotificationUpdateDto>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AvatarResponse.fromJson(entry.value);
|
final value = NotificationUpdateDto.fromJson(entry.value);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@ -78,14 +82,14 @@ class AvatarResponse {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of AvatarResponse-objects as value to a dart map
|
// maps a json object with a list of NotificationUpdateDto-objects as value to a dart map
|
||||||
static Map<String, List<AvatarResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
static Map<String, List<NotificationUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final map = <String, List<AvatarResponse>>{};
|
final map = <String, List<NotificationUpdateDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = AvatarResponse.listFromJson(entry.value, growable: growable,);
|
map[entry.key] = NotificationUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@ -93,7 +97,6 @@ class AvatarResponse {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'color',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
12
mobile/openapi/lib/model/permission.dart
generated
12
mobile/openapi/lib/model/permission.dart
generated
@ -66,6 +66,10 @@ class Permission {
|
|||||||
static const memoryPeriodRead = Permission._(r'memory.read');
|
static const memoryPeriodRead = Permission._(r'memory.read');
|
||||||
static const memoryPeriodUpdate = Permission._(r'memory.update');
|
static const memoryPeriodUpdate = Permission._(r'memory.update');
|
||||||
static const memoryPeriodDelete = Permission._(r'memory.delete');
|
static const memoryPeriodDelete = Permission._(r'memory.delete');
|
||||||
|
static const notificationPeriodCreate = Permission._(r'notification.create');
|
||||||
|
static const notificationPeriodRead = Permission._(r'notification.read');
|
||||||
|
static const notificationPeriodUpdate = Permission._(r'notification.update');
|
||||||
|
static const notificationPeriodDelete = Permission._(r'notification.delete');
|
||||||
static const partnerPeriodCreate = Permission._(r'partner.create');
|
static const partnerPeriodCreate = Permission._(r'partner.create');
|
||||||
static const partnerPeriodRead = Permission._(r'partner.read');
|
static const partnerPeriodRead = Permission._(r'partner.read');
|
||||||
static const partnerPeriodUpdate = Permission._(r'partner.update');
|
static const partnerPeriodUpdate = Permission._(r'partner.update');
|
||||||
@ -147,6 +151,10 @@ class Permission {
|
|||||||
memoryPeriodRead,
|
memoryPeriodRead,
|
||||||
memoryPeriodUpdate,
|
memoryPeriodUpdate,
|
||||||
memoryPeriodDelete,
|
memoryPeriodDelete,
|
||||||
|
notificationPeriodCreate,
|
||||||
|
notificationPeriodRead,
|
||||||
|
notificationPeriodUpdate,
|
||||||
|
notificationPeriodDelete,
|
||||||
partnerPeriodCreate,
|
partnerPeriodCreate,
|
||||||
partnerPeriodRead,
|
partnerPeriodRead,
|
||||||
partnerPeriodUpdate,
|
partnerPeriodUpdate,
|
||||||
@ -263,6 +271,10 @@ class PermissionTypeTransformer {
|
|||||||
case r'memory.read': return Permission.memoryPeriodRead;
|
case r'memory.read': return Permission.memoryPeriodRead;
|
||||||
case r'memory.update': return Permission.memoryPeriodUpdate;
|
case r'memory.update': return Permission.memoryPeriodUpdate;
|
||||||
case r'memory.delete': return Permission.memoryPeriodDelete;
|
case r'memory.delete': return Permission.memoryPeriodDelete;
|
||||||
|
case r'notification.create': return Permission.notificationPeriodCreate;
|
||||||
|
case r'notification.read': return Permission.notificationPeriodRead;
|
||||||
|
case r'notification.update': return Permission.notificationPeriodUpdate;
|
||||||
|
case r'notification.delete': return Permission.notificationPeriodDelete;
|
||||||
case r'partner.create': return Permission.partnerPeriodCreate;
|
case r'partner.create': return Permission.partnerPeriodCreate;
|
||||||
case r'partner.read': return Permission.partnerPeriodRead;
|
case r'partner.read': return Permission.partnerPeriodRead;
|
||||||
case r'partner.update': return Permission.partnerPeriodUpdate;
|
case r'partner.update': return Permission.partnerPeriodUpdate;
|
||||||
|
13
mobile/openapi/lib/model/user_admin_create_dto.dart
generated
13
mobile/openapi/lib/model/user_admin_create_dto.dart
generated
@ -13,6 +13,7 @@ part of openapi.api;
|
|||||||
class UserAdminCreateDto {
|
class UserAdminCreateDto {
|
||||||
/// Returns a new [UserAdminCreateDto] instance.
|
/// Returns a new [UserAdminCreateDto] instance.
|
||||||
UserAdminCreateDto({
|
UserAdminCreateDto({
|
||||||
|
this.avatarColor,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.notify,
|
this.notify,
|
||||||
@ -22,6 +23,8 @@ class UserAdminCreateDto {
|
|||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserAvatarColor? avatarColor;
|
||||||
|
|
||||||
String email;
|
String email;
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
@ -51,6 +54,7 @@ class UserAdminCreateDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto &&
|
||||||
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.notify == notify &&
|
other.notify == notify &&
|
||||||
@ -62,6 +66,7 @@ class UserAdminCreateDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email.hashCode) +
|
(email.hashCode) +
|
||||||
(name.hashCode) +
|
(name.hashCode) +
|
||||||
(notify == null ? 0 : notify!.hashCode) +
|
(notify == null ? 0 : notify!.hashCode) +
|
||||||
@ -71,10 +76,15 @@ class UserAdminCreateDto {
|
|||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserAdminCreateDto[email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.avatarColor != null) {
|
||||||
|
json[r'avatarColor'] = this.avatarColor;
|
||||||
|
} else {
|
||||||
|
// json[r'avatarColor'] = null;
|
||||||
|
}
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
json[r'name'] = this.name;
|
json[r'name'] = this.name;
|
||||||
if (this.notify != null) {
|
if (this.notify != null) {
|
||||||
@ -110,6 +120,7 @@ class UserAdminCreateDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserAdminCreateDto(
|
return UserAdminCreateDto(
|
||||||
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email')!,
|
email: mapValueOfType<String>(json, r'email')!,
|
||||||
name: mapValueOfType<String>(json, r'name')!,
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
notify: mapValueOfType<bool>(json, r'notify'),
|
notify: mapValueOfType<bool>(json, r'notify'),
|
||||||
|
13
mobile/openapi/lib/model/user_admin_update_dto.dart
generated
13
mobile/openapi/lib/model/user_admin_update_dto.dart
generated
@ -13,6 +13,7 @@ part of openapi.api;
|
|||||||
class UserAdminUpdateDto {
|
class UserAdminUpdateDto {
|
||||||
/// Returns a new [UserAdminUpdateDto] instance.
|
/// Returns a new [UserAdminUpdateDto] instance.
|
||||||
UserAdminUpdateDto({
|
UserAdminUpdateDto({
|
||||||
|
this.avatarColor,
|
||||||
this.email,
|
this.email,
|
||||||
this.name,
|
this.name,
|
||||||
this.password,
|
this.password,
|
||||||
@ -21,6 +22,8 @@ class UserAdminUpdateDto {
|
|||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserAvatarColor? avatarColor;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -60,6 +63,7 @@ class UserAdminUpdateDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto &&
|
||||||
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
@ -70,6 +74,7 @@ class UserAdminUpdateDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
@ -78,10 +83,15 @@ class UserAdminUpdateDto {
|
|||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserAdminUpdateDto[email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.avatarColor != null) {
|
||||||
|
json[r'avatarColor'] = this.avatarColor;
|
||||||
|
} else {
|
||||||
|
// json[r'avatarColor'] = null;
|
||||||
|
}
|
||||||
if (this.email != null) {
|
if (this.email != null) {
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
} else {
|
} else {
|
||||||
@ -124,6 +134,7 @@ class UserAdminUpdateDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserAdminUpdateDto(
|
return UserAdminUpdateDto(
|
||||||
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
|
@ -13,7 +13,6 @@ part of openapi.api;
|
|||||||
class UserPreferencesResponseDto {
|
class UserPreferencesResponseDto {
|
||||||
/// Returns a new [UserPreferencesResponseDto] instance.
|
/// Returns a new [UserPreferencesResponseDto] instance.
|
||||||
UserPreferencesResponseDto({
|
UserPreferencesResponseDto({
|
||||||
required this.avatar,
|
|
||||||
required this.download,
|
required this.download,
|
||||||
required this.emailNotifications,
|
required this.emailNotifications,
|
||||||
required this.folders,
|
required this.folders,
|
||||||
@ -25,8 +24,6 @@ class UserPreferencesResponseDto {
|
|||||||
required this.tags,
|
required this.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
AvatarResponse avatar;
|
|
||||||
|
|
||||||
DownloadResponse download;
|
DownloadResponse download;
|
||||||
|
|
||||||
EmailNotificationsResponse emailNotifications;
|
EmailNotificationsResponse emailNotifications;
|
||||||
@ -47,7 +44,6 @@ class UserPreferencesResponseDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
|
||||||
other.avatar == avatar &&
|
|
||||||
other.download == download &&
|
other.download == download &&
|
||||||
other.emailNotifications == emailNotifications &&
|
other.emailNotifications == emailNotifications &&
|
||||||
other.folders == folders &&
|
other.folders == folders &&
|
||||||
@ -61,7 +57,6 @@ class UserPreferencesResponseDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(avatar.hashCode) +
|
|
||||||
(download.hashCode) +
|
(download.hashCode) +
|
||||||
(emailNotifications.hashCode) +
|
(emailNotifications.hashCode) +
|
||||||
(folders.hashCode) +
|
(folders.hashCode) +
|
||||||
@ -73,11 +68,10 @@ class UserPreferencesResponseDto {
|
|||||||
(tags.hashCode);
|
(tags.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
|
String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'avatar'] = this.avatar;
|
|
||||||
json[r'download'] = this.download;
|
json[r'download'] = this.download;
|
||||||
json[r'emailNotifications'] = this.emailNotifications;
|
json[r'emailNotifications'] = this.emailNotifications;
|
||||||
json[r'folders'] = this.folders;
|
json[r'folders'] = this.folders;
|
||||||
@ -99,7 +93,6 @@ class UserPreferencesResponseDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserPreferencesResponseDto(
|
return UserPreferencesResponseDto(
|
||||||
avatar: AvatarResponse.fromJson(json[r'avatar'])!,
|
|
||||||
download: DownloadResponse.fromJson(json[r'download'])!,
|
download: DownloadResponse.fromJson(json[r'download'])!,
|
||||||
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
|
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
|
||||||
folders: FoldersResponse.fromJson(json[r'folders'])!,
|
folders: FoldersResponse.fromJson(json[r'folders'])!,
|
||||||
@ -156,7 +149,6 @@ class UserPreferencesResponseDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'avatar',
|
|
||||||
'download',
|
'download',
|
||||||
'emailNotifications',
|
'emailNotifications',
|
||||||
'folders',
|
'folders',
|
||||||
|
13
mobile/openapi/lib/model/user_update_me_dto.dart
generated
13
mobile/openapi/lib/model/user_update_me_dto.dart
generated
@ -13,11 +13,14 @@ part of openapi.api;
|
|||||||
class UserUpdateMeDto {
|
class UserUpdateMeDto {
|
||||||
/// Returns a new [UserUpdateMeDto] instance.
|
/// Returns a new [UserUpdateMeDto] instance.
|
||||||
UserUpdateMeDto({
|
UserUpdateMeDto({
|
||||||
|
this.avatarColor,
|
||||||
this.email,
|
this.email,
|
||||||
this.name,
|
this.name,
|
||||||
this.password,
|
this.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserAvatarColor? avatarColor;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -44,6 +47,7 @@ class UserUpdateMeDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
|
||||||
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.password == password;
|
other.password == password;
|
||||||
@ -51,15 +55,21 @@ class UserUpdateMeDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode);
|
(password == null ? 0 : password!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserUpdateMeDto[email=$email, name=$name, password=$password]';
|
String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.avatarColor != null) {
|
||||||
|
json[r'avatarColor'] = this.avatarColor;
|
||||||
|
} else {
|
||||||
|
// json[r'avatarColor'] = null;
|
||||||
|
}
|
||||||
if (this.email != null) {
|
if (this.email != null) {
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
} else {
|
} else {
|
||||||
@ -87,6 +97,7 @@ class UserUpdateMeDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserUpdateMeDto(
|
return UserUpdateMeDto(
|
||||||
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.132.1+195
|
version: 1.132.3+197
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
@ -60,6 +60,9 @@ void main() {
|
|||||||
final MockAlbumMediaRepository albumMediaRepository =
|
final MockAlbumMediaRepository albumMediaRepository =
|
||||||
MockAlbumMediaRepository();
|
MockAlbumMediaRepository();
|
||||||
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
||||||
|
final MockAppSettingService appSettingService = MockAppSettingService();
|
||||||
|
final MockLocalFilesManagerRepository localFilesManagerRepository =
|
||||||
|
MockLocalFilesManagerRepository();
|
||||||
final MockPartnerApiRepository partnerApiRepository =
|
final MockPartnerApiRepository partnerApiRepository =
|
||||||
MockPartnerApiRepository();
|
MockPartnerApiRepository();
|
||||||
final MockUserApiRepository userApiRepository = MockUserApiRepository();
|
final MockUserApiRepository userApiRepository = MockUserApiRepository();
|
||||||
@ -106,6 +109,8 @@ void main() {
|
|||||||
userRepository,
|
userRepository,
|
||||||
userService,
|
userService,
|
||||||
eTagRepository,
|
eTagRepository,
|
||||||
|
appSettingService,
|
||||||
|
localFilesManagerRepository,
|
||||||
partnerApiRepository,
|
partnerApiRepository,
|
||||||
userApiRepository,
|
userApiRepository,
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
|||||||
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
@ -41,6 +42,9 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
|
|||||||
|
|
||||||
class MockAuthRepository extends Mock implements IAuthRepository {}
|
class MockAuthRepository extends Mock implements IAuthRepository {}
|
||||||
|
|
||||||
|
class MockPartnerRepository extends Mock implements IPartnerRepository {}
|
||||||
|
|
||||||
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
|
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
|
||||||
|
|
||||||
class MockPartnerRepository extends Mock implements IPartnerRepository {}
|
class MockLocalFilesManagerRepository extends Mock
|
||||||
|
implements ILocalFilesManager {}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/entity.service.dart';
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
@ -25,4 +26,6 @@ class MockNetworkService extends Mock implements NetworkService {}
|
|||||||
|
|
||||||
class MockSearchApi extends Mock implements SearchApi {}
|
class MockSearchApi extends Mock implements SearchApi {}
|
||||||
|
|
||||||
|
class MockAppSettingService extends Mock implements AppSettingsService {}
|
||||||
|
|
||||||
class MockBackgroundService extends Mock implements BackgroundService {}
|
class MockBackgroundService extends Mock implements BackgroundService {}
|
||||||
|
@ -206,6 +206,141 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/notifications": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "createNotification",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationCreateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications (Admin)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/notifications/templates/{name}": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "getNotificationTemplateAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TemplateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TemplateResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications (Admin)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/notifications/test-email": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "sendTestEmailAdmin",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TestEmailResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications (Admin)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/users": {
|
"/admin/users": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "searchUsersAdmin",
|
"operationId": "searchUsersAdmin",
|
||||||
@ -3485,15 +3620,224 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/notifications/admin/templates/{name}": {
|
"/notifications": {
|
||||||
"post": {
|
"delete": {
|
||||||
"operationId": "getNotificationTemplateAdmin",
|
"operationId": "deleteNotifications",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDeleteAllDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getNotifications",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "id",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "level",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationLevel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationType"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unread",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateNotifications",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationUpdateAllDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteNotification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getNotification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateNotification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3502,7 +3846,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/TemplateDto"
|
"$ref": "#/components/schemas/NotificationUpdateDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3513,7 +3857,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/TemplateResponseDto"
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3532,49 +3876,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Notifications (Admin)"
|
"Notifications"
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/notifications/admin/test-email": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "sendTestEmailAdmin",
|
|
||||||
"parameters": [],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TestEmailResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Notifications (Admin)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -7656,7 +7958,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@ -8884,21 +9186,6 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"AvatarResponse": {
|
|
||||||
"properties": {
|
|
||||||
"color": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UserAvatarColor"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"color"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"AvatarUpdate": {
|
"AvatarUpdate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"color": {
|
"color": {
|
||||||
@ -10341,6 +10628,157 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"NotificationCreateDto": {
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationLevel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationDeleteAllDto": {
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"items": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationDto": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationLevel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"id",
|
||||||
|
"level",
|
||||||
|
"title",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationLevel": {
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error",
|
||||||
|
"warning",
|
||||||
|
"info"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"NotificationType": {
|
||||||
|
"enum": [
|
||||||
|
"JobFailed",
|
||||||
|
"BackupFailed",
|
||||||
|
"SystemMessage",
|
||||||
|
"Custom"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"NotificationUpdateAllDto": {
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"items": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationUpdateDto": {
|
||||||
|
"properties": {
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"OAuthAuthorizeResponseDto": {
|
"OAuthAuthorizeResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"url": {
|
"url": {
|
||||||
@ -10615,6 +11053,10 @@
|
|||||||
"memory.read",
|
"memory.read",
|
||||||
"memory.update",
|
"memory.update",
|
||||||
"memory.delete",
|
"memory.delete",
|
||||||
|
"notification.create",
|
||||||
|
"notification.read",
|
||||||
|
"notification.update",
|
||||||
|
"notification.delete",
|
||||||
"partner.create",
|
"partner.create",
|
||||||
"partner.read",
|
"partner.read",
|
||||||
"partner.update",
|
"partner.update",
|
||||||
@ -13621,6 +14063,14 @@
|
|||||||
},
|
},
|
||||||
"UserAdminCreateDto": {
|
"UserAdminCreateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -13763,6 +14213,14 @@
|
|||||||
},
|
},
|
||||||
"UserAdminUpdateDto": {
|
"UserAdminUpdateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -13826,9 +14284,6 @@
|
|||||||
},
|
},
|
||||||
"UserPreferencesResponseDto": {
|
"UserPreferencesResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatar": {
|
|
||||||
"$ref": "#/components/schemas/AvatarResponse"
|
|
||||||
},
|
|
||||||
"download": {
|
"download": {
|
||||||
"$ref": "#/components/schemas/DownloadResponse"
|
"$ref": "#/components/schemas/DownloadResponse"
|
||||||
},
|
},
|
||||||
@ -13858,7 +14313,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"avatar",
|
|
||||||
"download",
|
"download",
|
||||||
"emailNotifications",
|
"emailNotifications",
|
||||||
"folders",
|
"folders",
|
||||||
@ -13952,6 +14406,14 @@
|
|||||||
},
|
},
|
||||||
"UserUpdateMeDto": {
|
"UserUpdateMeDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.132.1
|
* 1.132.3
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@ -39,6 +39,48 @@ export type ActivityCreateDto = {
|
|||||||
export type ActivityStatisticsResponseDto = {
|
export type ActivityStatisticsResponseDto = {
|
||||||
comments: number;
|
comments: number;
|
||||||
};
|
};
|
||||||
|
export type NotificationCreateDto = {
|
||||||
|
data?: object;
|
||||||
|
description?: string | null;
|
||||||
|
level?: NotificationLevel;
|
||||||
|
readAt?: string | null;
|
||||||
|
title: string;
|
||||||
|
"type"?: NotificationType;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
export type NotificationDto = {
|
||||||
|
createdAt: string;
|
||||||
|
data?: object;
|
||||||
|
description?: string;
|
||||||
|
id: string;
|
||||||
|
level: NotificationLevel;
|
||||||
|
readAt?: string;
|
||||||
|
title: string;
|
||||||
|
"type": NotificationType;
|
||||||
|
};
|
||||||
|
export type TemplateDto = {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
export type TemplateResponseDto = {
|
||||||
|
html: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
export type SystemConfigSmtpTransportDto = {
|
||||||
|
host: string;
|
||||||
|
ignoreCert: boolean;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
export type SystemConfigSmtpDto = {
|
||||||
|
enabled: boolean;
|
||||||
|
"from": string;
|
||||||
|
replyTo: string;
|
||||||
|
transport: SystemConfigSmtpTransportDto;
|
||||||
|
};
|
||||||
|
export type TestEmailResponseDto = {
|
||||||
|
messageId: string;
|
||||||
|
};
|
||||||
export type UserLicense = {
|
export type UserLicense = {
|
||||||
activatedAt: string;
|
activatedAt: string;
|
||||||
activationKey: string;
|
activationKey: string;
|
||||||
@ -64,6 +106,7 @@ export type UserAdminResponseDto = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
export type UserAdminCreateDto = {
|
export type UserAdminCreateDto = {
|
||||||
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
@ -76,6 +119,7 @@ export type UserAdminDeleteDto = {
|
|||||||
force?: boolean;
|
force?: boolean;
|
||||||
};
|
};
|
||||||
export type UserAdminUpdateDto = {
|
export type UserAdminUpdateDto = {
|
||||||
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@ -83,9 +127,6 @@ export type UserAdminUpdateDto = {
|
|||||||
shouldChangePassword?: boolean;
|
shouldChangePassword?: boolean;
|
||||||
storageLabel?: string | null;
|
storageLabel?: string | null;
|
||||||
};
|
};
|
||||||
export type AvatarResponse = {
|
|
||||||
color: UserAvatarColor;
|
|
||||||
};
|
|
||||||
export type DownloadResponse = {
|
export type DownloadResponse = {
|
||||||
archiveSize: number;
|
archiveSize: number;
|
||||||
includeEmbeddedVideos: boolean;
|
includeEmbeddedVideos: boolean;
|
||||||
@ -122,7 +163,6 @@ export type TagsResponse = {
|
|||||||
sidebarWeb: boolean;
|
sidebarWeb: boolean;
|
||||||
};
|
};
|
||||||
export type UserPreferencesResponseDto = {
|
export type UserPreferencesResponseDto = {
|
||||||
avatar: AvatarResponse;
|
|
||||||
download: DownloadResponse;
|
download: DownloadResponse;
|
||||||
emailNotifications: EmailNotificationsResponse;
|
emailNotifications: EmailNotificationsResponse;
|
||||||
folders: FoldersResponse;
|
folders: FoldersResponse;
|
||||||
@ -663,28 +703,15 @@ export type MemoryUpdateDto = {
|
|||||||
memoryAt?: string;
|
memoryAt?: string;
|
||||||
seenAt?: string;
|
seenAt?: string;
|
||||||
};
|
};
|
||||||
export type TemplateDto = {
|
export type NotificationDeleteAllDto = {
|
||||||
template: string;
|
ids: string[];
|
||||||
};
|
};
|
||||||
export type TemplateResponseDto = {
|
export type NotificationUpdateAllDto = {
|
||||||
html: string;
|
ids: string[];
|
||||||
name: string;
|
readAt?: string | null;
|
||||||
};
|
};
|
||||||
export type SystemConfigSmtpTransportDto = {
|
export type NotificationUpdateDto = {
|
||||||
host: string;
|
readAt?: string | null;
|
||||||
ignoreCert: boolean;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
export type SystemConfigSmtpDto = {
|
|
||||||
enabled: boolean;
|
|
||||||
"from": string;
|
|
||||||
replyTo: string;
|
|
||||||
transport: SystemConfigSmtpTransportDto;
|
|
||||||
};
|
|
||||||
export type TestEmailResponseDto = {
|
|
||||||
messageId: string;
|
|
||||||
};
|
};
|
||||||
export type OAuthConfigDto = {
|
export type OAuthConfigDto = {
|
||||||
codeChallenge?: string;
|
codeChallenge?: string;
|
||||||
@ -1388,6 +1415,7 @@ export type TrashResponseDto = {
|
|||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
export type UserUpdateMeDto = {
|
export type UserUpdateMeDto = {
|
||||||
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@ -1454,6 +1482,43 @@ export function deleteActivity({ id }: {
|
|||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function createNotification({ notificationCreateDto }: {
|
||||||
|
notificationCreateDto: NotificationCreateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 201;
|
||||||
|
data: NotificationDto;
|
||||||
|
}>("/admin/notifications", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: notificationCreateDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function getNotificationTemplateAdmin({ name, templateDto }: {
|
||||||
|
name: string;
|
||||||
|
templateDto: TemplateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: TemplateResponseDto;
|
||||||
|
}>(`/admin/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: templateDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
||||||
|
systemConfigSmtpDto: SystemConfigSmtpDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: TestEmailResponseDto;
|
||||||
|
}>("/admin/notifications/test-email", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: systemConfigSmtpDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
export function searchUsersAdmin({ withDeleted }: {
|
export function searchUsersAdmin({ withDeleted }: {
|
||||||
withDeleted?: boolean;
|
withDeleted?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
@ -2322,29 +2387,71 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
|
|||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getNotificationTemplateAdmin({ name, templateDto }: {
|
export function deleteNotifications({ notificationDeleteAllDto }: {
|
||||||
name: string;
|
notificationDeleteAllDto: NotificationDeleteAllDto;
|
||||||
templateDto: TemplateDto;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
|
||||||
status: 200;
|
|
||||||
data: TemplateResponseDto;
|
|
||||||
}>(`/notifications/admin/templates/${encodeURIComponent(name)}`, oazapfts.json({
|
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "DELETE",
|
||||||
body: templateDto
|
body: notificationDeleteAllDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
export function getNotifications({ id, level, $type, unread }: {
|
||||||
systemConfigSmtpDto: SystemConfigSmtpDto;
|
id?: string;
|
||||||
|
level?: NotificationLevel;
|
||||||
|
$type?: NotificationType;
|
||||||
|
unread?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: TestEmailResponseDto;
|
data: NotificationDto[];
|
||||||
}>("/notifications/admin/test-email", oazapfts.json({
|
}>(`/notifications${QS.query(QS.explode({
|
||||||
|
id,
|
||||||
|
level,
|
||||||
|
"type": $type,
|
||||||
|
unread
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function updateNotifications({ notificationUpdateAllDto }: {
|
||||||
|
notificationUpdateAllDto: NotificationUpdateAllDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "PUT",
|
||||||
body: systemConfigSmtpDto
|
body: notificationUpdateAllDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function deleteNotification({ id }: {
|
||||||
|
id: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText(`/notifications/${encodeURIComponent(id)}`, {
|
||||||
|
...opts,
|
||||||
|
method: "DELETE"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function getNotification({ id }: {
|
||||||
|
id: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: NotificationDto;
|
||||||
|
}>(`/notifications/${encodeURIComponent(id)}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function updateNotification({ id, notificationUpdateDto }: {
|
||||||
|
id: string;
|
||||||
|
notificationUpdateDto: NotificationUpdateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: NotificationDto;
|
||||||
|
}>(`/notifications/${encodeURIComponent(id)}`, oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "PUT",
|
||||||
|
body: notificationUpdateDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function startOAuth({ oAuthConfigDto }: {
|
export function startOAuth({ oAuthConfigDto }: {
|
||||||
@ -3453,6 +3560,18 @@ export enum UserAvatarColor {
|
|||||||
Gray = "gray",
|
Gray = "gray",
|
||||||
Amber = "amber"
|
Amber = "amber"
|
||||||
}
|
}
|
||||||
|
export enum NotificationLevel {
|
||||||
|
Success = "success",
|
||||||
|
Error = "error",
|
||||||
|
Warning = "warning",
|
||||||
|
Info = "info"
|
||||||
|
}
|
||||||
|
export enum NotificationType {
|
||||||
|
JobFailed = "JobFailed",
|
||||||
|
BackupFailed = "BackupFailed",
|
||||||
|
SystemMessage = "SystemMessage",
|
||||||
|
Custom = "Custom"
|
||||||
|
}
|
||||||
export enum UserStatus {
|
export enum UserStatus {
|
||||||
Active = "active",
|
Active = "active",
|
||||||
Removing = "removing",
|
Removing = "removing",
|
||||||
@ -3527,6 +3646,10 @@ export enum Permission {
|
|||||||
MemoryRead = "memory.read",
|
MemoryRead = "memory.read",
|
||||||
MemoryUpdate = "memory.update",
|
MemoryUpdate = "memory.update",
|
||||||
MemoryDelete = "memory.delete",
|
MemoryDelete = "memory.delete",
|
||||||
|
NotificationCreate = "notification.create",
|
||||||
|
NotificationRead = "notification.read",
|
||||||
|
NotificationUpdate = "notification.update",
|
||||||
|
NotificationDelete = "notification.delete",
|
||||||
PartnerCreate = "partner.create",
|
PartnerCreate = "partner.create",
|
||||||
PartnerRead = "partner.read",
|
PartnerRead = "partner.read",
|
||||||
PartnerUpdate = "partner.update",
|
PartnerUpdate = "partner.update",
|
||||||
|
70
server/package-lock.json
generated
70
server/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -32,6 +32,7 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"compression": "^1.8.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
@ -83,6 +84,7 @@
|
|||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
@ -5009,6 +5011,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/compression": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
@ -7603,6 +7615,60 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.0.2",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.132.1",
|
"version": "1.132.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -57,6 +57,7 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"compression": "^1.8.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
@ -108,6 +109,7 @@
|
|||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
|
@ -44,7 +44,7 @@ const imports = [
|
|||||||
BullModule.registerQueue(...bull.queues),
|
BullModule.registerQueue(...bull.queues),
|
||||||
ClsModule.forRoot(cls.config),
|
ClsModule.forRoot(cls.config),
|
||||||
OpenTelemetryModule.forRoot(otel),
|
OpenTelemetryModule.forRoot(otel),
|
||||||
KyselyModule.forRoot(getKyselyConfig(database.config.kysely)),
|
KyselyModule.forRoot(getKyselyConfig(database.config)),
|
||||||
];
|
];
|
||||||
|
|
||||||
class BaseModule implements OnModuleInit, OnModuleDestroy {
|
class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||||
|
@ -10,7 +10,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository';
|
|||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import 'src/schema';
|
import 'src/schema';
|
||||||
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
||||||
import { getKyselyConfig } from 'src/utils/database';
|
import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
@ -56,7 +56,7 @@ const main = async () => {
|
|||||||
const getDatabaseClient = () => {
|
const getDatabaseClient = () => {
|
||||||
const configRepository = new ConfigRepository();
|
const configRepository = new ConfigRepository();
|
||||||
const { database } = configRepository.getEnv();
|
const { database } = configRepository.getEnv();
|
||||||
return new Kysely<any>(getKyselyConfig(database.config.kysely));
|
return new Kysely<any>(getKyselyConfig(database.config));
|
||||||
};
|
};
|
||||||
|
|
||||||
const runQuery = async (query: string) => {
|
const runQuery = async (query: string) => {
|
||||||
@ -105,7 +105,7 @@ const create = (path: string, up: string[], down: string[]) => {
|
|||||||
const compare = async () => {
|
const compare = async () => {
|
||||||
const configRepository = new ConfigRepository();
|
const configRepository = new ConfigRepository();
|
||||||
const { database } = configRepository.getEnv();
|
const { database } = configRepository.getEnv();
|
||||||
const db = postgres(database.config.kysely);
|
const db = postgres(asPostgresConnectionConfig(database.config));
|
||||||
|
|
||||||
const source = schemaFromCode();
|
const source = schemaFromCode();
|
||||||
const target = await schemaFromDatabase(db, {});
|
const target = await schemaFromDatabase(db, {});
|
||||||
|
@ -78,7 +78,7 @@ class SqlGenerator {
|
|||||||
const moduleFixture = await Test.createTestingModule({
|
const moduleFixture = await Test.createTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
KyselyModule.forRoot({
|
KyselyModule.forRoot({
|
||||||
...getKyselyConfig(database.config.kysely),
|
...getKyselyConfig(database.config),
|
||||||
log: (event) => {
|
log: (event) => {
|
||||||
if (event.level === 'query') {
|
if (event.level === 'query') {
|
||||||
this.sqlLogger.logQuery(event.query.sql);
|
this.sqlLogger.logQuery(event.query.sql);
|
||||||
|
@ -14,6 +14,7 @@ import { LibraryController } from 'src/controllers/library.controller';
|
|||||||
import { MapController } from 'src/controllers/map.controller';
|
import { MapController } from 'src/controllers/map.controller';
|
||||||
import { MemoryController } from 'src/controllers/memory.controller';
|
import { MemoryController } from 'src/controllers/memory.controller';
|
||||||
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
||||||
|
import { NotificationController } from 'src/controllers/notification.controller';
|
||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
@ -47,6 +48,7 @@ export const controllers = [
|
|||||||
LibraryController,
|
LibraryController,
|
||||||
MapController,
|
MapController,
|
||||||
MemoryController,
|
MemoryController,
|
||||||
|
NotificationController,
|
||||||
NotificationAdminController,
|
NotificationAdminController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
|
import {
|
||||||
|
NotificationCreateDto,
|
||||||
|
NotificationDto,
|
||||||
|
TemplateDto,
|
||||||
|
TemplateResponseDto,
|
||||||
|
TestEmailResponseDto,
|
||||||
|
} from 'src/dtos/notification.dto';
|
||||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||||
import { NotificationService } from 'src/services/notification.service';
|
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||||
|
|
||||||
@ApiTags('Notifications (Admin)')
|
@ApiTags('Notifications (Admin)')
|
||||||
@Controller('notifications/admin')
|
@Controller('admin/notifications')
|
||||||
export class NotificationAdminController {
|
export class NotificationAdminController {
|
||||||
constructor(private service: NotificationService) {}
|
constructor(private service: NotificationAdminService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authenticated({ admin: true })
|
||||||
|
createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise<NotificationDto> {
|
||||||
|
return this.service.create(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('test-email')
|
@Post('test-email')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
60
server/src/controllers/notification.controller.ts
Normal file
60
server/src/controllers/notification.controller.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import {
|
||||||
|
NotificationDeleteAllDto,
|
||||||
|
NotificationDto,
|
||||||
|
NotificationSearchDto,
|
||||||
|
NotificationUpdateAllDto,
|
||||||
|
NotificationUpdateDto,
|
||||||
|
} from 'src/dtos/notification.dto';
|
||||||
|
import { Permission } from 'src/enum';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
|
@ApiTags('Notifications')
|
||||||
|
@Controller('notifications')
|
||||||
|
export class NotificationController {
|
||||||
|
constructor(private service: NotificationService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_READ })
|
||||||
|
getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> {
|
||||||
|
return this.service.search(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
|
||||||
|
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
|
||||||
|
return this.service.updateAll(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
|
||||||
|
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
|
||||||
|
return this.service.deleteAll(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_READ })
|
||||||
|
getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> {
|
||||||
|
return this.service.get(auth, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
|
||||||
|
updateNotification(
|
||||||
|
@Auth() auth: AuthDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Body() dto: NotificationUpdateDto,
|
||||||
|
): Promise<NotificationDto> {
|
||||||
|
return this.service.update(auth, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
|
||||||
|
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
return this.service.delete(auth, id);
|
||||||
|
}
|
||||||
|
}
|
@ -90,7 +90,7 @@ export class StorageCore {
|
|||||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
|
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: ImageFormat) {
|
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') {
|
||||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
|
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
Permission,
|
Permission,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
SourceType,
|
SourceType,
|
||||||
|
UserAvatarColor,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||||
@ -122,6 +123,7 @@ export type User = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
avatarColor: UserAvatarColor | null;
|
||||||
profileImagePath: string;
|
profileImagePath: string;
|
||||||
profileChangedAt: Date;
|
profileChangedAt: Date;
|
||||||
};
|
};
|
||||||
@ -264,7 +266,15 @@ export type AssetFace = {
|
|||||||
person?: Person | null;
|
person?: Person | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const;
|
||||||
|
const userWithPrefixColumns = [
|
||||||
|
'users.id',
|
||||||
|
'users.name',
|
||||||
|
'users.email',
|
||||||
|
'users.avatarColor',
|
||||||
|
'users.profileImagePath',
|
||||||
|
'users.profileChangedAt',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const columns = {
|
export const columns = {
|
||||||
asset: [
|
asset: [
|
||||||
@ -306,7 +316,7 @@ export const columns = {
|
|||||||
'shared_links.password',
|
'shared_links.password',
|
||||||
],
|
],
|
||||||
user: userColumns,
|
user: userColumns,
|
||||||
userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'],
|
userWithPrefix: userWithPrefixColumns,
|
||||||
userAdmin: [
|
userAdmin: [
|
||||||
...userColumns,
|
...userColumns,
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -323,6 +333,7 @@ export const columns = {
|
|||||||
],
|
],
|
||||||
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
|
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
|
||||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||||
|
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
|
||||||
syncAsset: [
|
syncAsset: [
|
||||||
'id',
|
'id',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
|
18
server/src/db.d.ts
vendored
18
server/src/db.d.ts
vendored
@ -11,6 +11,8 @@ import {
|
|||||||
AssetStatus,
|
AssetStatus,
|
||||||
AssetType,
|
AssetType,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
|
NotificationLevel,
|
||||||
|
NotificationType,
|
||||||
Permission,
|
Permission,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
SourceType,
|
SourceType,
|
||||||
@ -263,6 +265,21 @@ export interface Memories {
|
|||||||
updateId: Generated<string>;
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Notifications {
|
||||||
|
id: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
updateId: Generated<string>;
|
||||||
|
userId: string;
|
||||||
|
level: Generated<NotificationLevel>;
|
||||||
|
type: NotificationType;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
data: any | null;
|
||||||
|
readAt: Timestamp | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MemoriesAssetsAssets {
|
export interface MemoriesAssetsAssets {
|
||||||
assetsId: string;
|
assetsId: string;
|
||||||
memoriesId: string;
|
memoriesId: string;
|
||||||
@ -463,6 +480,7 @@ export interface DB {
|
|||||||
memories: Memories;
|
memories: Memories;
|
||||||
memories_assets_assets: MemoriesAssetsAssets;
|
memories_assets_assets: MemoriesAssetsAssets;
|
||||||
migrations: Migrations;
|
migrations: Migrations;
|
||||||
|
notifications: Notifications;
|
||||||
move_history: MoveHistory;
|
move_history: MoveHistory;
|
||||||
naturalearth_countries: NaturalearthCountries;
|
naturalearth_countries: NaturalearthCountries;
|
||||||
partners_audit: PartnersAudit;
|
partners_audit: PartnersAudit;
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { IsString } from 'class-validator';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsString } from 'class-validator';
|
||||||
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
|
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class TestEmailResponseDto {
|
export class TestEmailResponseDto {
|
||||||
messageId!: string;
|
messageId!: string;
|
||||||
@ -11,3 +14,106 @@ export class TemplateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
template!: string;
|
template!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotificationDto {
|
||||||
|
id!: string;
|
||||||
|
@ValidateDate()
|
||||||
|
createdAt!: Date;
|
||||||
|
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||||
|
level!: NotificationLevel;
|
||||||
|
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||||
|
type!: NotificationType;
|
||||||
|
title!: string;
|
||||||
|
description?: string;
|
||||||
|
data?: any;
|
||||||
|
readAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationSearchDto {
|
||||||
|
@Optional()
|
||||||
|
@ValidateUUID({ optional: true })
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
@IsEnum(NotificationLevel)
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||||
|
level?: NotificationLevel;
|
||||||
|
|
||||||
|
@IsEnum(NotificationType)
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||||
|
type?: NotificationType;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
unread?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationCreateDto {
|
||||||
|
@Optional()
|
||||||
|
@IsEnum(NotificationLevel)
|
||||||
|
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||||
|
level?: NotificationLevel;
|
||||||
|
|
||||||
|
@IsEnum(NotificationType)
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||||
|
type?: NotificationType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
data?: any;
|
||||||
|
|
||||||
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
|
||||||
|
@ValidateUUID()
|
||||||
|
userId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationUpdateDto {
|
||||||
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationUpdateAllDto {
|
||||||
|
@ValidateUUID({ each: true, optional: true })
|
||||||
|
ids!: string[];
|
||||||
|
|
||||||
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationDeleteAllDto {
|
||||||
|
@ValidateUUID({ each: true })
|
||||||
|
ids!: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapNotification = {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updateId?: string;
|
||||||
|
level: NotificationLevel;
|
||||||
|
type: NotificationType;
|
||||||
|
data: any | null;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
readAt: Date | null;
|
||||||
|
};
|
||||||
|
export const mapNotification = (notification: MapNotification): NotificationDto => {
|
||||||
|
return {
|
||||||
|
id: notification.id,
|
||||||
|
createdAt: notification.createdAt,
|
||||||
|
level: notification.level,
|
||||||
|
type: notification.type,
|
||||||
|
title: notification.title,
|
||||||
|
description: notification.description ?? undefined,
|
||||||
|
data: notification.data ?? undefined,
|
||||||
|
readAt: notification.readAt ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -137,11 +137,6 @@ export class UserPreferencesUpdateDto {
|
|||||||
purchase?: PurchaseUpdate;
|
purchase?: PurchaseUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvatarResponse {
|
|
||||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
|
||||||
color!: UserAvatarColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RatingsResponse {
|
class RatingsResponse {
|
||||||
enabled: boolean = false;
|
enabled: boolean = false;
|
||||||
}
|
}
|
||||||
@ -195,7 +190,6 @@ export class UserPreferencesResponseDto implements UserPreferences {
|
|||||||
ratings!: RatingsResponse;
|
ratings!: RatingsResponse;
|
||||||
sharedLinks!: SharedLinksResponse;
|
sharedLinks!: SharedLinksResponse;
|
||||||
tags!: TagsResponse;
|
tags!: TagsResponse;
|
||||||
avatar!: AvatarResponse;
|
|
||||||
emailNotifications!: EmailNotificationsResponse;
|
emailNotifications!: EmailNotificationsResponse;
|
||||||
download!: DownloadResponse;
|
download!: DownloadResponse;
|
||||||
purchase!: PurchaseResponse;
|
purchase!: PurchaseResponse;
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||||
import { User, UserAdmin } from 'src/database';
|
import { User, UserAdmin } from 'src/database';
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { UserMetadataItem } from 'src/types';
|
||||||
import { getPreferences } from 'src/utils/preferences';
|
|
||||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||||
|
|
||||||
export class UserUpdateMeDto {
|
export class UserUpdateMeDto {
|
||||||
@ -23,6 +22,11 @@ export class UserUpdateMeDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserResponseDto {
|
export class UserResponseDto {
|
||||||
@ -41,13 +45,21 @@ export class UserLicense {
|
|||||||
activatedAt!: Date;
|
activatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailToAvatarColor = (email: string): UserAvatarColor => {
|
||||||
|
const values = Object.values(UserAvatarColor);
|
||||||
|
const randomIndex = Math.floor(
|
||||||
|
[...email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
||||||
|
);
|
||||||
|
return values[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
email: entity.email,
|
email: entity.email,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color,
|
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
|
||||||
profileChangedAt: entity.profileChangedAt,
|
profileChangedAt: entity.profileChangedAt,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -69,6 +81,11 @@ export class UserAdminCreateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
@ -104,6 +121,11 @@ export class UserAdminUpdateDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
|
@ -126,6 +126,11 @@ export enum Permission {
|
|||||||
MEMORY_UPDATE = 'memory.update',
|
MEMORY_UPDATE = 'memory.update',
|
||||||
MEMORY_DELETE = 'memory.delete',
|
MEMORY_DELETE = 'memory.delete',
|
||||||
|
|
||||||
|
NOTIFICATION_CREATE = 'notification.create',
|
||||||
|
NOTIFICATION_READ = 'notification.read',
|
||||||
|
NOTIFICATION_UPDATE = 'notification.update',
|
||||||
|
NOTIFICATION_DELETE = 'notification.delete',
|
||||||
|
|
||||||
PARTNER_CREATE = 'partner.create',
|
PARTNER_CREATE = 'partner.create',
|
||||||
PARTNER_READ = 'partner.read',
|
PARTNER_READ = 'partner.read',
|
||||||
PARTNER_UPDATE = 'partner.update',
|
PARTNER_UPDATE = 'partner.update',
|
||||||
@ -332,6 +337,11 @@ export enum ImageFormat {
|
|||||||
WEBP = 'webp',
|
WEBP = 'webp',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RawExtractedFormat {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
JXL = 'jxl',
|
||||||
|
}
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
VERBOSE = 'verbose',
|
VERBOSE = 'verbose',
|
||||||
DEBUG = 'debug',
|
DEBUG = 'debug',
|
||||||
@ -515,6 +525,7 @@ export enum JobName {
|
|||||||
NOTIFY_SIGNUP = 'notify-signup',
|
NOTIFY_SIGNUP = 'notify-signup',
|
||||||
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
||||||
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
||||||
|
NOTIFICATIONS_CLEANUP = 'notifications-cleanup',
|
||||||
SEND_EMAIL = 'notification-send-email',
|
SEND_EMAIL = 'notification-send-email',
|
||||||
|
|
||||||
// Version check
|
// Version check
|
||||||
@ -580,3 +591,17 @@ export enum SyncEntityType {
|
|||||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||||
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum NotificationLevel {
|
||||||
|
Success = 'success',
|
||||||
|
Error = 'error',
|
||||||
|
Warning = 'warning',
|
||||||
|
Info = 'info',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
JobFailed = 'JobFailed',
|
||||||
|
BackupFailed = 'BackupFailed',
|
||||||
|
SystemMessage = 'SystemMessage',
|
||||||
|
Custom = 'Custom',
|
||||||
|
}
|
||||||
|
@ -157,6 +157,15 @@ where
|
|||||||
and "memories"."ownerId" = $2
|
and "memories"."ownerId" = $2
|
||||||
and "memories"."deletedAt" is null
|
and "memories"."deletedAt" is null
|
||||||
|
|
||||||
|
-- AccessRepository.notification.checkOwnerAccess
|
||||||
|
select
|
||||||
|
"notifications"."id"
|
||||||
|
from
|
||||||
|
"notifications"
|
||||||
|
where
|
||||||
|
"notifications"."id" in ($1)
|
||||||
|
and "notifications"."userId" = $2
|
||||||
|
|
||||||
-- AccessRepository.person.checkOwnerAccess
|
-- AccessRepository.person.checkOwnerAccess
|
||||||
select
|
select
|
||||||
"person"."id"
|
"person"."id"
|
||||||
|
@ -13,6 +13,7 @@ from
|
|||||||
"users"."id",
|
"users"."id",
|
||||||
"users"."name",
|
"users"."name",
|
||||||
"users"."email",
|
"users"."email",
|
||||||
|
"users"."avatarColor",
|
||||||
"users"."profileImagePath",
|
"users"."profileImagePath",
|
||||||
"users"."profileChangedAt"
|
"users"."profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -44,6 +45,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
|
@ -12,6 +12,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -36,6 +37,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -100,6 +102,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -124,6 +127,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -191,6 +195,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -215,6 +220,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -269,6 +275,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -292,6 +299,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -353,6 +361,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
|
@ -259,6 +259,130 @@ from
|
|||||||
where
|
where
|
||||||
"assets"."id" = $2
|
"assets"."id" = $2
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForSyncAssets
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."isOffline",
|
||||||
|
"assets"."libraryId",
|
||||||
|
"assets"."originalPath",
|
||||||
|
"assets"."status",
|
||||||
|
"assets"."fileModifiedAt"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."id" = any ($1::uuid[])
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForAssetDeletion
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."isVisible",
|
||||||
|
"assets"."libraryId",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."livePhotoVideoId",
|
||||||
|
"assets"."sidecarPath",
|
||||||
|
"assets"."encodedVideoPath",
|
||||||
|
"assets"."originalPath",
|
||||||
|
to_json("exif") as "exifInfo",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_faces".*,
|
||||||
|
"person" as "person"
|
||||||
|
from
|
||||||
|
"asset_faces"
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
"person".*
|
||||||
|
from
|
||||||
|
"person"
|
||||||
|
where
|
||||||
|
"asset_faces"."personId" = "person"."id"
|
||||||
|
) as "person" on true
|
||||||
|
where
|
||||||
|
"asset_faces"."assetId" = "assets"."id"
|
||||||
|
and "asset_faces"."deletedAt" is null
|
||||||
|
) as agg
|
||||||
|
) as "faces",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_files"."id",
|
||||||
|
"asset_files"."path",
|
||||||
|
"asset_files"."type"
|
||||||
|
from
|
||||||
|
"asset_files"
|
||||||
|
where
|
||||||
|
"asset_files"."assetId" = "assets"."id"
|
||||||
|
) as agg
|
||||||
|
) as "files",
|
||||||
|
to_json("stacked_assets") as "stack"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||||
|
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
"asset_stack"."id",
|
||||||
|
"asset_stack"."primaryAssetId",
|
||||||
|
array_agg("stacked") as "assets"
|
||||||
|
from
|
||||||
|
"assets" as "stacked"
|
||||||
|
where
|
||||||
|
"stacked"."deletedAt" is not null
|
||||||
|
and "stacked"."isArchived" = $1
|
||||||
|
and "stacked"."stackId" = "asset_stack"."id"
|
||||||
|
group by
|
||||||
|
"asset_stack"."id"
|
||||||
|
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||||
|
where
|
||||||
|
"assets"."id" = $2
|
||||||
|
|
||||||
|
-- AssetJobRepository.streamForVideoConversion
|
||||||
|
select
|
||||||
|
"assets"."id"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."type" = $1
|
||||||
|
and (
|
||||||
|
"assets"."encodedVideoPath" is null
|
||||||
|
or "assets"."encodedVideoPath" = $2
|
||||||
|
)
|
||||||
|
and "assets"."isVisible" = $3
|
||||||
|
and "assets"."deletedAt" is null
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForVideoConversion
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."originalPath",
|
||||||
|
"assets"."encodedVideoPath"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."id" = $1
|
||||||
|
and "assets"."type" = $2
|
||||||
|
|
||||||
|
-- AssetJobRepository.streamForMetadataExtraction
|
||||||
|
select
|
||||||
|
"assets"."id"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
left join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id"
|
||||||
|
where
|
||||||
|
(
|
||||||
|
"asset_job_status"."metadataExtractedAt" is null
|
||||||
|
or "asset_job_status"."assetId" is null
|
||||||
|
)
|
||||||
|
and "assets"."isVisible" = $1
|
||||||
|
and "assets"."deletedAt" is null
|
||||||
|
|
||||||
-- AssetJobRepository.getForStorageTemplateJob
|
-- AssetJobRepository.getForStorageTemplateJob
|
||||||
select
|
select
|
||||||
"assets"."id",
|
"assets"."id",
|
||||||
|
58
server/src/queries/notification.repository.sql
Normal file
58
server/src/queries/notification.repository.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
|
-- NotificationRepository.cleanup
|
||||||
|
delete from "notifications"
|
||||||
|
where
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"deletedAt" is not null
|
||||||
|
and "deletedAt" < $1
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
"readAt" > $2
|
||||||
|
and "createdAt" < $3
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
"readAt" = $4
|
||||||
|
and "createdAt" < $5
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- NotificationRepository.search
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"level",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"data",
|
||||||
|
"readAt"
|
||||||
|
from
|
||||||
|
"notifications"
|
||||||
|
where
|
||||||
|
"userId" = $1
|
||||||
|
and "deletedAt" is null
|
||||||
|
order by
|
||||||
|
"createdAt" desc
|
||||||
|
|
||||||
|
-- NotificationRepository.search (unread)
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"level",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"data",
|
||||||
|
"readAt"
|
||||||
|
from
|
||||||
|
"notifications"
|
||||||
|
where
|
||||||
|
(
|
||||||
|
"userId" = $1
|
||||||
|
and "readAt" is null
|
||||||
|
)
|
||||||
|
and "deletedAt" is null
|
||||||
|
order by
|
||||||
|
"createdAt" desc
|
@ -12,6 +12,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -29,6 +30,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -61,6 +63,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -78,6 +81,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -112,6 +116,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -129,6 +134,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -156,6 +162,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -173,6 +180,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
|
@ -5,6 +5,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -43,6 +44,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -90,6 +92,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -128,6 +131,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -152,6 +156,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -198,6 +203,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -235,6 +241,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
@ -279,6 +279,26 @@ class AuthDeviceAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NotificationAccess {
|
||||||
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||||
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
|
async checkOwnerAccess(userId: string, notificationIds: Set<string>) {
|
||||||
|
if (notificationIds.size === 0) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select('notifications.id')
|
||||||
|
.where('notifications.id', 'in', [...notificationIds])
|
||||||
|
.where('notifications.userId', '=', userId)
|
||||||
|
.execute()
|
||||||
|
.then((stacks) => new Set(stacks.map((stack) => stack.id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class StackAccess {
|
class StackAccess {
|
||||||
constructor(private db: Kysely<DB>) {}
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@ -426,6 +446,7 @@ export class AccessRepository {
|
|||||||
asset: AssetAccess;
|
asset: AssetAccess;
|
||||||
authDevice: AuthDeviceAccess;
|
authDevice: AuthDeviceAccess;
|
||||||
memory: MemoryAccess;
|
memory: MemoryAccess;
|
||||||
|
notification: NotificationAccess;
|
||||||
person: PersonAccess;
|
person: PersonAccess;
|
||||||
partner: PartnerAccess;
|
partner: PartnerAccess;
|
||||||
stack: StackAccess;
|
stack: StackAccess;
|
||||||
@ -438,6 +459,7 @@ export class AccessRepository {
|
|||||||
this.asset = new AssetAccess(db);
|
this.asset = new AssetAccess(db);
|
||||||
this.authDevice = new AuthDeviceAccess(db);
|
this.authDevice = new AuthDeviceAccess(db);
|
||||||
this.memory = new MemoryAccess(db);
|
this.memory = new MemoryAccess(db);
|
||||||
|
this.notification = new NotificationAccess(db);
|
||||||
this.person = new PersonAccess(db);
|
this.person = new PersonAccess(db);
|
||||||
this.partner = new PartnerAccess(db);
|
this.partner = new PartnerAccess(db);
|
||||||
this.stack = new StackAccess(db);
|
this.stack = new StackAccess(db);
|
||||||
|
@ -2,12 +2,21 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { Asset, columns } from 'src/database';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetFileType } from 'src/enum';
|
import { AssetFileType, AssetType } from 'src/enum';
|
||||||
import { StorageAsset } from 'src/types';
|
import { StorageAsset } from 'src/types';
|
||||||
import { anyUuid, asUuid, withExifInner, withFaces, withFiles } from 'src/utils/database';
|
import {
|
||||||
|
anyUuid,
|
||||||
|
asUuid,
|
||||||
|
toJson,
|
||||||
|
withExif,
|
||||||
|
withExifInner,
|
||||||
|
withFaces,
|
||||||
|
withFacesAndPeople,
|
||||||
|
withFiles,
|
||||||
|
} from 'src/utils/database';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetJobRepository {
|
export class AssetJobRepository {
|
||||||
@ -148,6 +157,7 @@ export class AssetJobRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
getForSyncAssets(ids: string[]) {
|
getForSyncAssets(ids: string[]) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
@ -163,6 +173,84 @@ export class AssetJobRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
getForAssetDeletion(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select([
|
||||||
|
'assets.id',
|
||||||
|
'assets.isVisible',
|
||||||
|
'assets.libraryId',
|
||||||
|
'assets.ownerId',
|
||||||
|
'assets.livePhotoVideoId',
|
||||||
|
'assets.sidecarPath',
|
||||||
|
'assets.encodedVideoPath',
|
||||||
|
'assets.originalPath',
|
||||||
|
])
|
||||||
|
.$call(withExif)
|
||||||
|
.select(withFacesAndPeople)
|
||||||
|
.select(withFiles)
|
||||||
|
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||||
|
.leftJoinLateral(
|
||||||
|
(eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('assets as stacked')
|
||||||
|
.select(['asset_stack.id', 'asset_stack.primaryAssetId'])
|
||||||
|
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
|
||||||
|
.where('stacked.deletedAt', 'is not', null)
|
||||||
|
.where('stacked.isArchived', '=', false)
|
||||||
|
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||||
|
.groupBy('asset_stack.id')
|
||||||
|
.as('stacked_assets'),
|
||||||
|
(join) => join.on('asset_stack.id', 'is not', null),
|
||||||
|
)
|
||||||
|
.select((eb) => toJson(eb, 'stacked_assets').as('stack'))
|
||||||
|
.where('assets.id', '=', id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [], stream: true })
|
||||||
|
streamForVideoConversion(force?: boolean) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(['assets.id'])
|
||||||
|
.where('assets.type', '=', AssetType.VIDEO)
|
||||||
|
.$if(!force, (qb) =>
|
||||||
|
qb
|
||||||
|
.where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')]))
|
||||||
|
.where('assets.isVisible', '=', true),
|
||||||
|
)
|
||||||
|
.where('assets.deletedAt', 'is', null)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
getForVideoConversion(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(['assets.id', 'assets.ownerId', 'assets.originalPath', 'assets.encodedVideoPath'])
|
||||||
|
.where('assets.id', '=', id)
|
||||||
|
.where('assets.type', '=', AssetType.VIDEO)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [], stream: true })
|
||||||
|
streamForMetadataExtraction(force?: boolean) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(['assets.id'])
|
||||||
|
.$if(!force, (qb) =>
|
||||||
|
qb
|
||||||
|
.leftJoin('asset_job_status', 'asset_job_status.assetId', 'assets.id')
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]),
|
||||||
|
)
|
||||||
|
.where('assets.isVisible', '=', true),
|
||||||
|
)
|
||||||
|
.where('assets.deletedAt', 'is', null)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
private storageTemplateAssetQuery() {
|
private storageTemplateAssetQuery() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
@ -80,21 +80,12 @@ describe('getEnv', () => {
|
|||||||
const { database } = getEnv();
|
const { database } = getEnv();
|
||||||
expect(database).toEqual({
|
expect(database).toEqual({
|
||||||
config: {
|
config: {
|
||||||
kysely: expect.objectContaining({
|
connectionType: 'parts',
|
||||||
host: 'database',
|
host: 'database',
|
||||||
port: 5432,
|
port: 5432,
|
||||||
database: 'immich',
|
database: 'immich',
|
||||||
username: 'postgres',
|
username: 'postgres',
|
||||||
password: 'postgres',
|
password: 'postgres',
|
||||||
}),
|
|
||||||
typeorm: expect.objectContaining({
|
|
||||||
type: 'postgres',
|
|
||||||
host: 'database',
|
|
||||||
port: 5432,
|
|
||||||
database: 'immich',
|
|
||||||
username: 'postgres',
|
|
||||||
password: 'postgres',
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
skipMigrations: false,
|
skipMigrations: false,
|
||||||
vectorExtension: 'vectors',
|
vectorExtension: 'vectors',
|
||||||
@ -110,88 +101,9 @@ describe('getEnv', () => {
|
|||||||
it('should use DB_URL', () => {
|
it('should use DB_URL', () => {
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich';
|
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich';
|
||||||
const { database } = getEnv();
|
const { database } = getEnv();
|
||||||
expect(database.config.kysely).toMatchObject({
|
expect(database.config).toMatchObject({
|
||||||
host: 'database1',
|
connectionType: 'url',
|
||||||
password: 'postgres2',
|
url: 'postgres://postgres1:postgres2@database1:54320/immich',
|
||||||
user: 'postgres1',
|
|
||||||
port: 54_320,
|
|
||||||
database: 'immich',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle sslmode=require', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle sslmode=prefer', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle sslmode=verify-ca', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle sslmode=verify-full', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle sslmode=no-verify', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({ ssl: { rejectUnauthorized: false } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle ssl=true', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({ ssl: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject invalid ssl', () => {
|
|
||||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid';
|
|
||||||
|
|
||||||
expect(() => getEnv()).toThrowError('Invalid ssl option: invalid');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle socket: URLs', () => {
|
|
||||||
process.env.DB_URL = 'socket:/run/postgresql?db=database1';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({
|
|
||||||
host: '/run/postgresql',
|
|
||||||
database: 'database1',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle sockets in postgres: URLs', () => {
|
|
||||||
process.env.DB_URL = 'postgres:///database2?host=/path/to/socket';
|
|
||||||
|
|
||||||
const { database } = getEnv();
|
|
||||||
|
|
||||||
expect(database.config.kysely).toMatchObject({
|
|
||||||
host: '/path/to/socket',
|
|
||||||
database: 'database2',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,8 +7,7 @@ import { Request, Response } from 'express';
|
|||||||
import { RedisOptions } from 'ioredis';
|
import { RedisOptions } from 'ioredis';
|
||||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||||
import { join, resolve } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { parse } from 'pg-connection-string';
|
|
||||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||||
import { Telemetry } from 'src/decorators';
|
import { Telemetry } from 'src/decorators';
|
||||||
import { EnvDto } from 'src/dtos/env.dto';
|
import { EnvDto } from 'src/dtos/env.dto';
|
||||||
@ -22,9 +21,7 @@ import {
|
|||||||
QueueName,
|
QueueName,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
||||||
import { isValidSsl, PostgresConnectionConfig } from 'src/utils/database';
|
|
||||||
import { setDifference } from 'src/utils/set';
|
import { setDifference } from 'src/utils/set';
|
||||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
|
||||||
|
|
||||||
export interface EnvData {
|
export interface EnvData {
|
||||||
host?: string;
|
host?: string;
|
||||||
@ -59,7 +56,7 @@ export interface EnvData {
|
|||||||
};
|
};
|
||||||
|
|
||||||
database: {
|
database: {
|
||||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: PostgresConnectionConfig };
|
config: DatabaseConnectionParams;
|
||||||
skipMigrations: boolean;
|
skipMigrations: boolean;
|
||||||
vectorExtension: VectorExtension;
|
vectorExtension: VectorExtension;
|
||||||
};
|
};
|
||||||
@ -152,14 +149,10 @@ const getEnv = (): EnvData => {
|
|||||||
const isProd = environment === ImmichEnvironment.PRODUCTION;
|
const isProd = environment === ImmichEnvironment.PRODUCTION;
|
||||||
const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
|
const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
|
||||||
const folders = {
|
const folders = {
|
||||||
// eslint-disable-next-line unicorn/prefer-module
|
|
||||||
dist: resolve(`${__dirname}/..`),
|
|
||||||
geodata: join(buildFolder, 'geodata'),
|
geodata: join(buildFolder, 'geodata'),
|
||||||
web: join(buildFolder, 'www'),
|
web: join(buildFolder, 'www'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const databaseUrl = dto.DB_URL;
|
|
||||||
|
|
||||||
let redisConfig = {
|
let redisConfig = {
|
||||||
host: dto.REDIS_HOSTNAME || 'redis',
|
host: dto.REDIS_HOSTNAME || 'redis',
|
||||||
port: dto.REDIS_PORT || 6379,
|
port: dto.REDIS_PORT || 6379,
|
||||||
@ -191,30 +184,16 @@ const getEnv = (): EnvData => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = {
|
const databaseConnection: DatabaseConnectionParams = dto.DB_URL
|
||||||
connectionType: 'parts',
|
? { connectionType: 'url', url: dto.DB_URL }
|
||||||
host: dto.DB_HOSTNAME || 'database',
|
: {
|
||||||
port: dto.DB_PORT || 5432,
|
connectionType: 'parts',
|
||||||
username: dto.DB_USERNAME || 'postgres',
|
host: dto.DB_HOSTNAME || 'database',
|
||||||
password: dto.DB_PASSWORD || 'postgres',
|
port: dto.DB_PORT || 5432,
|
||||||
database: dto.DB_DATABASE_NAME || 'immich',
|
username: dto.DB_USERNAME || 'postgres',
|
||||||
} as const;
|
password: dto.DB_PASSWORD || 'postgres',
|
||||||
|
database: dto.DB_DATABASE_NAME || 'immich',
|
||||||
let parsedOptions: PostgresConnectionConfig = parts;
|
};
|
||||||
if (dto.DB_URL) {
|
|
||||||
const parsed = parse(dto.DB_URL);
|
|
||||||
if (!isValidSsl(parsed.ssl)) {
|
|
||||||
throw new Error(`Invalid ssl option: ${parsed.ssl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedOptions = {
|
|
||||||
...parsed,
|
|
||||||
ssl: parsed.ssl,
|
|
||||||
host: parsed.host ?? undefined,
|
|
||||||
port: parsed.port ? Number(parsed.port) : undefined,
|
|
||||||
database: parsed.database ?? undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
host: dto.IMMICH_HOST,
|
host: dto.IMMICH_HOST,
|
||||||
@ -269,21 +248,7 @@ const getEnv = (): EnvData => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
database: {
|
database: {
|
||||||
config: {
|
config: databaseConnection,
|
||||||
typeorm: {
|
|
||||||
type: 'postgres',
|
|
||||||
entities: [],
|
|
||||||
migrations: [`${folders.dist}/migrations` + '/*.{js,ts}'],
|
|
||||||
subscribers: [],
|
|
||||||
migrationsRun: false,
|
|
||||||
synchronize: false,
|
|
||||||
connectTimeoutMS: 10_000, // 10 seconds
|
|
||||||
parseInt8: true,
|
|
||||||
...(databaseUrl ? { connectionType: 'url', url: databaseUrl } : parts),
|
|
||||||
},
|
|
||||||
kysely: parsedOptions,
|
|
||||||
},
|
|
||||||
|
|
||||||
skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
|
skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
|
||||||
vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
|
vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,7 @@ import AsyncLock from 'async-lock';
|
|||||||
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
|
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { readdir } from 'node:fs/promises';
|
import { readdir } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
@ -205,8 +205,29 @@ export class DatabaseRepository {
|
|||||||
const { rows } = await tableExists.execute(this.db);
|
const { rows } = await tableExists.execute(this.db);
|
||||||
const hasTypeOrmMigrations = !!rows[0]?.result;
|
const hasTypeOrmMigrations = !!rows[0]?.result;
|
||||||
if (hasTypeOrmMigrations) {
|
if (hasTypeOrmMigrations) {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-module
|
||||||
|
const dist = resolve(`${__dirname}/..`);
|
||||||
|
|
||||||
this.logger.debug('Running typeorm migrations');
|
this.logger.debug('Running typeorm migrations');
|
||||||
const dataSource = new DataSource(database.config.typeorm);
|
const dataSource = new DataSource({
|
||||||
|
type: 'postgres',
|
||||||
|
entities: [],
|
||||||
|
subscribers: [],
|
||||||
|
migrations: [`${dist}/migrations` + '/*.{js,ts}'],
|
||||||
|
migrationsRun: false,
|
||||||
|
synchronize: false,
|
||||||
|
connectTimeoutMS: 10_000, // 10 seconds
|
||||||
|
parseInt8: true,
|
||||||
|
...(database.config.connectionType === 'url'
|
||||||
|
? { url: database.config.url }
|
||||||
|
: {
|
||||||
|
host: database.config.host,
|
||||||
|
port: database.config.port,
|
||||||
|
username: database.config.username,
|
||||||
|
password: database.config.password,
|
||||||
|
database: database.config.database,
|
||||||
|
}),
|
||||||
|
});
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
await dataSource.runMigrations(options);
|
await dataSource.runMigrations(options);
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
|
@ -14,6 +14,7 @@ import { SystemConfig } from 'src/config';
|
|||||||
import { EventConfig } from 'src/decorators';
|
import { EventConfig } from 'src/decorators';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
@ -64,6 +65,7 @@ type EventMap = {
|
|||||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||||
|
|
||||||
'job.start': [QueueName, JobItem];
|
'job.start': [QueueName, JobItem];
|
||||||
|
'job.failed': [{ job: JobItem; error: Error | any }];
|
||||||
|
|
||||||
// session events
|
// session events
|
||||||
'session.delete': [{ sessionId: string }];
|
'session.delete': [{ sessionId: string }];
|
||||||
@ -104,6 +106,7 @@ export interface ClientEventMap {
|
|||||||
on_server_version: [ServerVersionResponseDto];
|
on_server_version: [ServerVersionResponseDto];
|
||||||
on_config_update: [];
|
on_config_update: [];
|
||||||
on_new_release: [ReleaseNotification];
|
on_new_release: [ReleaseNotification];
|
||||||
|
on_notification: [NotificationDto];
|
||||||
on_session_delete: [string];
|
on_session_delete: [string];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
|||||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
@ -55,6 +56,7 @@ export const repositories = [
|
|||||||
CryptoRepository,
|
CryptoRepository,
|
||||||
DatabaseRepository,
|
DatabaseRepository,
|
||||||
DownloadRepository,
|
DownloadRepository,
|
||||||
|
EmailRepository,
|
||||||
EventRepository,
|
EventRepository,
|
||||||
JobRepository,
|
JobRepository,
|
||||||
LibraryRepository,
|
LibraryRepository,
|
||||||
@ -65,7 +67,7 @@ export const repositories = [
|
|||||||
MemoryRepository,
|
MemoryRepository,
|
||||||
MetadataRepository,
|
MetadataRepository,
|
||||||
MoveRepository,
|
MoveRepository,
|
||||||
EmailRepository,
|
NotificationRepository,
|
||||||
OAuthRepository,
|
OAuthRepository,
|
||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
|
@ -7,7 +7,7 @@ import { Writable } from 'node:stream';
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
||||||
import { Exif } from 'src/database';
|
import { Exif } from 'src/database';
|
||||||
import { Colorspace, LogLevel } from 'src/enum';
|
import { Colorspace, LogLevel, RawExtractedFormat } from 'src/enum';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import {
|
import {
|
||||||
DecodeToBufferOptions,
|
DecodeToBufferOptions,
|
||||||
@ -36,34 +36,51 @@ type ProgressEvent = {
|
|||||||
percent?: number;
|
percent?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExtractResult = {
|
||||||
|
buffer: Buffer;
|
||||||
|
format: RawExtractedFormat;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaRepository {
|
export class MediaRepository {
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(private logger: LoggingRepository) {
|
||||||
this.logger.setContext(MediaRepository.name);
|
this.logger.setContext(MediaRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async extract(input: string, output: string): Promise<boolean> {
|
/**
|
||||||
|
*
|
||||||
|
* @param input file path to the input image
|
||||||
|
* @returns ExtractResult if succeeded, or null if failed
|
||||||
|
*/
|
||||||
|
async extract(input: string): Promise<ExtractResult | null> {
|
||||||
try {
|
try {
|
||||||
// remove existing output file if it exists
|
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
||||||
// as exiftool-vendored does not support overwriting via "-w!" flag
|
return { buffer, format: RawExtractedFormat.JPEG };
|
||||||
// and throws "1 files could not be read" error when the output file exists
|
} catch (error: any) {
|
||||||
await fs.unlink(output).catch(() => null);
|
this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message);
|
||||||
await exiftool.extractBinaryTag('JpgFromRaw2', input, output);
|
}
|
||||||
} catch {
|
|
||||||
try {
|
try {
|
||||||
this.logger.debug('Extracting JPEG from RAW image:', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
||||||
await exiftool.extractJpgFromRaw(input, output);
|
return { buffer, format: RawExtractedFormat.JPEG };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.debug('Could not extract JPEG from image, trying preview', error.message);
|
this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message);
|
||||||
try {
|
}
|
||||||
await exiftool.extractPreview(input, output);
|
|
||||||
} catch (error: any) {
|
try {
|
||||||
this.logger.debug('Could not extract preview from image', error.message);
|
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
||||||
return false;
|
return { buffer, format: RawExtractedFormat.JXL };
|
||||||
}
|
} catch (error: any) {
|
||||||
}
|
this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
||||||
|
return { buffer, format: RawExtractedFormat.JPEG };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.debug('Could not extract preview buffer from image', error.message);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
||||||
@ -104,7 +121,7 @@ export class MediaRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeImage(input: string, options: DecodeToBufferOptions) {
|
decodeImage(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||||
return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true });
|
return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +252,7 @@ export class MediaRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageDimensions(input: string): Promise<ImageDimensions> {
|
async getImageDimensions(input: string | Buffer): Promise<ImageDimensions> {
|
||||||
const { width = 0, height = 0 } = await sharp(input).metadata();
|
const { width = 0, height = 0 } = await sharp(input).metadata();
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
103
server/src/repositories/notification.repository.ts
Normal file
103
server/src/repositories/notification.repository.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { columns } from 'src/database';
|
||||||
|
import { DB, Notifications } from 'src/db';
|
||||||
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
|
import { NotificationSearchDto } from 'src/dtos/notification.dto';
|
||||||
|
|
||||||
|
export class NotificationRepository {
|
||||||
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
cleanup() {
|
||||||
|
return this.db
|
||||||
|
.deleteFrom('notifications')
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
// remove soft-deleted notifications
|
||||||
|
eb.and([eb('deletedAt', 'is not', null), eb('deletedAt', '<', DateTime.now().minus({ days: 3 }).toJSDate())]),
|
||||||
|
|
||||||
|
// remove old, read notifications
|
||||||
|
eb.and([
|
||||||
|
// keep recently read messages around for a few days
|
||||||
|
eb('readAt', '>', DateTime.now().minus({ days: 2 }).toJSDate()),
|
||||||
|
eb('createdAt', '<', DateTime.now().minus({ days: 15 }).toJSDate()),
|
||||||
|
]),
|
||||||
|
|
||||||
|
eb.and([
|
||||||
|
// remove super old, unread notifications
|
||||||
|
eb('readAt', '=', null),
|
||||||
|
eb('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, {}] }, { name: 'unread', params: [DummyValue.UUID, { unread: true }] })
|
||||||
|
search(userId: string, dto: NotificationSearchDto) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select(columns.notification)
|
||||||
|
.where((qb) =>
|
||||||
|
qb.and({
|
||||||
|
userId,
|
||||||
|
id: dto.id,
|
||||||
|
level: dto.level,
|
||||||
|
type: dto.type,
|
||||||
|
readAt: dto.unread ? null : undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.orderBy('createdAt', 'desc')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(notification: Insertable<Notifications>) {
|
||||||
|
return this.db
|
||||||
|
.insertInto('notifications')
|
||||||
|
.values(notification)
|
||||||
|
.returning(columns.notification)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select(columns.notification)
|
||||||
|
.where('id', '=', id)
|
||||||
|
.where('deletedAt', 'is not', null)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, notification: Updateable<Notifications>) {
|
||||||
|
return this.db
|
||||||
|
.updateTable('notifications')
|
||||||
|
.set(notification)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.where('id', '=', id)
|
||||||
|
.returning(columns.notification)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAll(ids: string[], notification: Updateable<Notifications>) {
|
||||||
|
await this.db.updateTable('notifications').set(notification).where('id', 'in', ids).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
await this.db
|
||||||
|
.updateTable('notifications')
|
||||||
|
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||||
|
.where('id', '=', id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(ids: string[]) {
|
||||||
|
await this.db
|
||||||
|
.updateTable('notifications')
|
||||||
|
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||||
|
.where('id', 'in', ids)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import { MemoryTable } from 'src/schema/tables/memory.table';
|
|||||||
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
||||||
import { MoveTable } from 'src/schema/tables/move.table';
|
import { MoveTable } from 'src/schema/tables/move.table';
|
||||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||||
|
import { NotificationTable } from 'src/schema/tables/notification.table';
|
||||||
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
||||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||||
import { PersonTable } from 'src/schema/tables/person.table';
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
@ -76,6 +77,7 @@ export class ImmichDatabase {
|
|||||||
MemoryTable,
|
MemoryTable,
|
||||||
MoveTable,
|
MoveTable,
|
||||||
NaturalEarthCountriesTable,
|
NaturalEarthCountriesTable,
|
||||||
|
NotificationTable,
|
||||||
PartnerAuditTable,
|
PartnerAuditTable,
|
||||||
PartnerTable,
|
PartnerTable,
|
||||||
PersonTable,
|
PersonTable,
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), "userId" uuid, "level" character varying NOT NULL DEFAULT 'info', "type" character varying NOT NULL DEFAULT 'info', "data" jsonb, "title" character varying NOT NULL, "description" text, "readAt" timestamp with time zone);`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" ADD CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" ADD CONSTRAINT "FK_692a909ee0fa9383e7859f9b406" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_notifications_update_id" ON "notifications" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_692a909ee0fa9383e7859f9b40" ON "notifications" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "notifications_updated_at"
|
||||||
|
BEFORE UPDATE ON "notifications"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "notifications_updated_at" ON "notifications";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_notifications_update_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_692a909ee0fa9383e7859f9b40";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" DROP CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" DROP CONSTRAINT "FK_692a909ee0fa9383e7859f9b406";`.execute(db);
|
||||||
|
await sql`DROP TABLE "notifications";`.execute(db);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "users" ADD "avatarColor" character varying;`.execute(db);
|
||||||
|
await sql`
|
||||||
|
UPDATE "users"
|
||||||
|
SET "avatarColor" = "user_metadata"."value"->'avatar'->>'color'
|
||||||
|
FROM "user_metadata"
|
||||||
|
WHERE "users"."id" = "user_metadata"."userId" AND "user_metadata"."key" = 'preferences';`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "users" DROP COLUMN "avatarColor";`.execute(db);
|
||||||
|
}
|
52
server/src/schema/tables/notification.table.ts
Normal file
52
server/src/schema/tables/notification.table.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
ForeignKeyColumn,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Table,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('notifications')
|
||||||
|
@UpdatedAtTrigger('notifications_updated_at')
|
||||||
|
export class NotificationTable {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt?: Date;
|
||||||
|
|
||||||
|
@UpdateIdColumn({ indexName: 'IDX_notifications_update_id' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@Column({ default: NotificationLevel.Info })
|
||||||
|
level!: NotificationLevel;
|
||||||
|
|
||||||
|
@Column({ default: NotificationLevel.Info })
|
||||||
|
type!: NotificationType;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
data!: any | null;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { ColumnType } from 'kysely';
|
import { ColumnType } from 'kysely';
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserStatus } from 'src/enum';
|
||||||
import { users_delete_audit } from 'src/schema/functions';
|
import { users_delete_audit } from 'src/schema/functions';
|
||||||
import {
|
import {
|
||||||
AfterDeleteTrigger,
|
AfterDeleteTrigger,
|
||||||
@ -49,6 +49,9 @@ export class UserTable {
|
|||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
shouldChangePassword!: Generated<boolean>;
|
shouldChangePassword!: Generated<boolean>;
|
||||||
|
|
||||||
|
@Column({ default: null })
|
||||||
|
avatarColor!: UserAvatarColor | null;
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt!: Timestamp | null;
|
deletedAt!: Timestamp | null;
|
||||||
|
|
||||||
|
@ -565,7 +565,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should remove faces', async () => {
|
it('should remove faces', async () => {
|
||||||
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
|
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
|
||||||
|
|
||||||
mocks.asset.getById.mockResolvedValue(assetWithFace);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetWithFace);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
|
||||||
|
|
||||||
@ -592,7 +592,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
||||||
mocks.stack.update.mockResolvedValue(factory.stack() as any);
|
mocks.stack.update.mockResolvedValue(factory.stack() as any);
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.primaryImage);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||||
|
|
||||||
@ -604,7 +604,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
||||||
mocks.stack.delete.mockResolvedValue();
|
mocks.stack.delete.mockResolvedValue();
|
||||||
mocks.asset.getById.mockResolvedValue({
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue({
|
||||||
...assetStub.primaryImage,
|
...assetStub.primaryImage,
|
||||||
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
|
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
|
||||||
});
|
});
|
||||||
@ -615,7 +615,7 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a live photo', async () => {
|
it('should delete a live photo', async () => {
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any);
|
||||||
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({
|
await sut.handleAssetDeletion({
|
||||||
@ -653,7 +653,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should not delete a live motion part if it is being used by another asset', async () => {
|
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||||
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({
|
await sut.handleAssetDeletion({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
@ -680,12 +680,13 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update usage', async () => {
|
it('should update usage', async () => {
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.image);
|
||||||
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
||||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if asset could not be found', async () => {
|
it('should fail if asset could not be found', async () => {
|
||||||
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0);
|
||||||
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
|
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
|
||||||
JobStatus.FAILED,
|
JobStatus.FAILED,
|
||||||
);
|
);
|
||||||
|
@ -189,13 +189,7 @@ export class AssetService extends BaseService {
|
|||||||
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
|
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
|
||||||
const { id, deleteOnDisk } = job;
|
const { id, deleteOnDisk } = job;
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetJobRepository.getForAssetDeletion(id);
|
||||||
faces: { person: true },
|
|
||||||
library: true,
|
|
||||||
stack: { assets: true },
|
|
||||||
exifInfo: true,
|
|
||||||
files: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
@ -142,52 +142,55 @@ describe(BackupService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||||
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run a database backup successfully', async () => {
|
it('should run a database backup successfully', async () => {
|
||||||
const result = await sut.handleBackupDatabase();
|
const result = await sut.handleBackupDatabase();
|
||||||
expect(result).toBe(JobStatus.SUCCESS);
|
expect(result).toBe(JobStatus.SUCCESS);
|
||||||
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename file on success', async () => {
|
it('should rename file on success', async () => {
|
||||||
const result = await sut.handleBackupDatabase();
|
const result = await sut.handleBackupDatabase();
|
||||||
expect(result).toBe(JobStatus.SUCCESS);
|
expect(result).toBe(JobStatus.SUCCESS);
|
||||||
expect(mocks.storage.rename).toHaveBeenCalled();
|
expect(mocks.storage.rename).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if pg_dumpall fails', async () => {
|
it('should fail if pg_dumpall fails', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if gzip fails', async () => {
|
it('should fail if gzip fails', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if write stream fails', async () => {
|
it('should fail if write stream fails', async () => {
|
||||||
mocks.storage.createWriteStream.mockImplementation(() => {
|
mocks.storage.createWriteStream.mockImplementation(() => {
|
||||||
throw new Error('error');
|
throw new Error('error');
|
||||||
});
|
});
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if rename fails', async () => {
|
it('should fail if rename fails', async () => {
|
||||||
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore unlink failing and still return failed job status', async () => {
|
it('should ignore unlink failing and still return failed job status', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||||
expect(mocks.storage.unlink).toHaveBeenCalled();
|
expect(mocks.storage.unlink).toHaveBeenCalled();
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
postgresVersion | expectedVersion
|
postgresVersion | expectedVersion
|
||||||
${'14.10'} | ${14}
|
${'14.10'} | ${14}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user