mirror of
https://github.com/immich-app/immich.git
synced 2025-12-11 09:13:08 +03:00
Compare commits
1 Commits
localizely
...
refactor-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8879790054 |
@@ -62,10 +62,6 @@ Instead of these experimental features, we recommend using the URL switching fea
|
||||
We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them.
|
||||
Please discuss any large PRs with our dev team to ensure your time is not wasted.
|
||||
|
||||
### Why isn't the mobile app updated yet?
|
||||
|
||||
The app stores can take a few days to approve new builds of the app. If you're impatient, android APKs can be downloaded from the GitHub releases.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
@@ -27,7 +27,7 @@ The script will perform the following actions:
|
||||
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
|
||||
2. Start the containers.
|
||||
|
||||
The web application and mobile app will be available at `http://<machine-ip-address>:2283`
|
||||
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`
|
||||
|
||||
The directory which is used to store the library files is `./immich-app` relative to the current directory.
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Login to the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283`
|
||||
Login to the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
|
||||
|
||||
<img src={require('./img/sign-in-phone.webp').default} width='50%' title='Mobile App Sign In' />
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
AssetMediaStatus,
|
||||
AssetResponseDto,
|
||||
AssetTypeEnum,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
getAssetInfo,
|
||||
getConfig,
|
||||
getMyUser,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
updateConfig,
|
||||
} from '@immich/sdk';
|
||||
import { exiftool } from 'exiftool-vendored';
|
||||
@@ -19,7 +19,7 @@ import { Socket } from 'socket.io-client';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -41,6 +41,8 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
||||
return dto;
|
||||
};
|
||||
|
||||
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
|
||||
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
const today = DateTime.now();
|
||||
@@ -462,55 +462,6 @@ describe('/search', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /search/random', () => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
]);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/search/random')
|
||||
.send({ size: 1 })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(1);
|
||||
expect(assets[0].ownerId).toBe(admin.userId);
|
||||
});
|
||||
|
||||
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/search/random')
|
||||
.send({ size: 2 })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(2);
|
||||
expect(assets[0].ownerId).toBe(admin.userId);
|
||||
expect(assets[1].ownerId).toBe(admin.userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /search/explore', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/search/explore');
|
||||
|
||||
@@ -76,7 +76,6 @@ export const immichCli = (args: string[]) =>
|
||||
export const immichAdmin = (args: string[]) =>
|
||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
const executeCommand = (command: string, args: string[]) => {
|
||||
let _resolve: (value: CommandResponse) => void;
|
||||
|
||||
@@ -53,7 +53,7 @@ show_friendly_message() {
|
||||
ip_address=$(hostname -I | awk '{print $1}')
|
||||
cat <<EOF
|
||||
Successfully deployed Immich!
|
||||
You can access the website or the mobile app at http://$ip_address:2283
|
||||
You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api
|
||||
---------------------------------------------------
|
||||
If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
|
||||
|
||||
|
||||
@@ -336,9 +336,9 @@
|
||||
"login_form_back_button_text": "الرجوع للخلف",
|
||||
"login_form_button_text": "تسجيل الدخول",
|
||||
"login_form_email_hint": "yoursemail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http: // your-server-ip: port/api",
|
||||
"login_form_endpoint_url": "url نقطة نهاية الخادم",
|
||||
"login_form_err_http": "يرجى تحديد http:// أو https://",
|
||||
"login_form_err_http": "يرجى تحديد http: // أو https: //",
|
||||
"login_form_err_invalid_email": "بريد إلكتروني خاطئ",
|
||||
"login_form_err_invalid_url": "URL غير صالح",
|
||||
"login_form_err_leading_whitespace": "قيادة المساحة البيضاء",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "فك الكومه",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Entra",
|
||||
"login_form_email_hint": "elteu@correu.cat",
|
||||
"login_form_endpoint_hint": "http://ip-del-servidor:port",
|
||||
"login_form_endpoint_hint": "http://ip-del-servidor:port/api",
|
||||
"login_form_endpoint_url": "URL del servidor",
|
||||
"login_form_err_http": "Especifica http:// o https://",
|
||||
"login_form_err_invalid_email": "Adreça de correu electrònic no vàlida",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Zpět",
|
||||
"login_form_button_text": "Přihlásit se",
|
||||
"login_form_email_hint": "tvůje-mail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-tvého-serveru:port",
|
||||
"login_form_endpoint_hint": "http://ip-tvého-serveru:port/api",
|
||||
"login_form_endpoint_url": "URL adresa serveru",
|
||||
"login_form_err_http": "Prosím, uveďte http:// nebo https://",
|
||||
"login_form_err_invalid_email": "Neplatný e-mail",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Tilbage",
|
||||
"login_form_button_text": "Log ind",
|
||||
"login_form_email_hint": "din-e-mail@e-mail.com",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Angiv venligst http:// eller https://",
|
||||
"login_form_err_invalid_email": "Ugyldig e-mail",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Zurück",
|
||||
"login_form_button_text": "Anmelden",
|
||||
"login_form_email_hint": "deine@email.de",
|
||||
"login_form_endpoint_hint": "http://deine-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server-URL",
|
||||
"login_form_err_http": "Bitte gebe http:// oder https:// an",
|
||||
"login_form_err_invalid_email": "Ungültige E-Mail",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Πίσω",
|
||||
"login_form_button_text": "Σύνδεση",
|
||||
"login_form_email_hint": "to-email-sou@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-tou-server-sou:porta",
|
||||
"login_form_endpoint_hint": "http://ip-tou-server-sou:porta/api",
|
||||
"login_form_endpoint_url": "URL τελικού σημείου διακομιστή",
|
||||
"login_form_err_http": "Προσδιορίστε http:// ή https://",
|
||||
"login_form_err_invalid_email": "Μη έγκυρο email",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates (EXPERIMENTAL)",
|
||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
||||
"advanced_settings_tile_title": "Advanced",
|
||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||
@@ -175,7 +175,7 @@
|
||||
"client_cert_remove": "Remove",
|
||||
"client_cert_remove_msg": "Client certificate is removed",
|
||||
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
|
||||
"client_cert_title": "SSL Client Certificate (EXPERIMENTAL)",
|
||||
"client_cert_title": "SSL Client Certificate",
|
||||
"common_add_to_album": "Add to album",
|
||||
"common_change_password": "Change Password",
|
||||
"common_create_new_album": "Create new album",
|
||||
@@ -281,7 +281,7 @@
|
||||
"header_settings_field_validator_msg": "Value cannot be empty",
|
||||
"header_settings_header_name_input": "Header name",
|
||||
"header_settings_header_value_input": "Header value",
|
||||
"header_settings_page_title": "Proxy Headers (EXPERIMENTAL)",
|
||||
"header_settings_page_title": "Proxy Headers",
|
||||
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
|
||||
"headers_settings_tile_title": "Custom proxy headers",
|
||||
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Atrás",
|
||||
"login_form_button_text": "Iniciar Sesión",
|
||||
"login_form_email_hint": "tucorreo@correo.com",
|
||||
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto",
|
||||
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
|
||||
"login_form_endpoint_url": "URL del servidor",
|
||||
"login_form_err_http": "Por favor, especifique http:// o https://",
|
||||
"login_form_err_invalid_email": "Correo electrónico no válido",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Iniciar sesión",
|
||||
"login_form_email_hint": "tucorreo@correo.com",
|
||||
"login_form_endpoint_hint": "http://la-ip-de-tu-servidor:puerto",
|
||||
"login_form_endpoint_hint": "http://la-ip-de-tu-servidor:puerto/api",
|
||||
"login_form_endpoint_url": "URL del servidor",
|
||||
"login_form_err_http": "Por favor, especifique http:// o https://",
|
||||
"login_form_err_invalid_email": "Correo electrónico inválido",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Iniciar sesión",
|
||||
"login_form_email_hint": "tucorreo@correo.com",
|
||||
"login_form_endpoint_hint": "http://la-ip-de-tu-servidor:puerto",
|
||||
"login_form_endpoint_hint": "http://la-ip-de-tu-servidor:puerto/api",
|
||||
"login_form_endpoint_url": "URL del servidor",
|
||||
"login_form_err_http": "Por favor, especifique http:// o https://",
|
||||
"login_form_err_invalid_email": "Correo electrónico inválido",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Iniciar sesión",
|
||||
"login_form_email_hint": "tucorreo@correo.com",
|
||||
"login_form_endpoint_hint": "http://ip-de-tu-servidor:puerto",
|
||||
"login_form_endpoint_hint": "http://ip-de-tu-servidor:puerto/api",
|
||||
"login_form_endpoint_url": "URL del servidor",
|
||||
"login_form_err_http": "Por favor, especifique http:// o https://",
|
||||
"login_form_err_invalid_email": "Correo electrónico inválido",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Takaisin",
|
||||
"login_form_button_text": "Kirjaudu",
|
||||
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
|
||||
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti",
|
||||
"login_form_endpoint_hint": "http://palvelimesi-osoite:portti/api",
|
||||
"login_form_endpoint_url": "Palvelimen URL",
|
||||
"login_form_err_http": "Lisää http:// tai https://",
|
||||
"login_form_err_invalid_email": "Virheellinen sähköpostiosoite",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Connexion",
|
||||
"login_form_email_hint": "votrecourriel@email.com",
|
||||
"login_form_endpoint_hint": "http://adresse-ip-serveur:port",
|
||||
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
|
||||
"login_form_endpoint_url": "URL du point d'accès au serveur",
|
||||
"login_form_err_http": "Veuillez préciser http:// ou https://",
|
||||
"login_form_err_invalid_email": "Courriel invalide",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Retour",
|
||||
"login_form_button_text": "Connexion",
|
||||
"login_form_email_hint": "votreemail@email.com",
|
||||
"login_form_endpoint_hint": "http://adresse-ip-serveur:port",
|
||||
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
|
||||
"login_form_endpoint_url": "URL du point d'accès au serveur",
|
||||
"login_form_err_http": "Veuillez préciser http:// ou https://",
|
||||
"login_form_err_invalid_email": "E-mail invalide",
|
||||
|
||||
@@ -336,9 +336,9 @@
|
||||
"login_form_back_button_text": "חזרה",
|
||||
"login_form_button_text": "התחברות",
|
||||
"login_form_email_hint": "yourmail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/API",
|
||||
"login_form_endpoint_url": "כתובת נקודת קצה השרת",
|
||||
"login_form_err_http": "נא לציין //:http או //:https",
|
||||
"login_form_err_http": "נא לציין //:htttp או //:https",
|
||||
"login_form_err_invalid_email": "דוא\"ל שגוי",
|
||||
"login_form_err_invalid_url": "כתובת לא חוקית",
|
||||
"login_form_err_leading_whitespace": "רווח לבן מוביל",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "ביטול ערימה",
|
||||
"wifi_name": "שם אינטרנט אלחוטי",
|
||||
"your_wifi_name": "שם אינטרנט אלחוטי שלך"
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Vissza",
|
||||
"login_form_button_text": "Bejelentkezés",
|
||||
"login_form_email_hint": "email@cimed.hu",
|
||||
"login_form_endpoint_hint": "http://szerver-címe:port",
|
||||
"login_form_endpoint_hint": "http(s)://szerver-címe:port/api",
|
||||
"login_form_endpoint_url": "Szerver címe",
|
||||
"login_form_err_http": "Kérjük, hogy egy http:// vagy https:// címet adj meg",
|
||||
"login_form_err_invalid_email": "Érvénytelen email cím",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "Csoport Megszűntetése",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Kembali",
|
||||
"login_form_button_text": "Masuk",
|
||||
"login_form_email_hint": "emailmu@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-server-anda:port",
|
||||
"login_form_endpoint_hint": "http://ip-server-anda:port/api",
|
||||
"login_form_endpoint_url": "URL Endpoint Server",
|
||||
"login_form_err_http": "Harap tentukan http:// atau https://",
|
||||
"login_form_err_invalid_email": "Email Tidak Valid",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Indietro",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "tuaemail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-del-tuo-server:port",
|
||||
"login_form_endpoint_hint": "http://ip-del-tuo-server:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Per favore specificare http:// o https://",
|
||||
"login_form_err_invalid_email": "Email non valida",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "戻る",
|
||||
"login_form_button_text": "ログイン",
|
||||
"login_form_email_hint": "hoge@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "https://example.com:port/api",
|
||||
"login_form_endpoint_url": "サーバーのエンドポイントURL",
|
||||
"login_form_err_http": "http://かhttps://かを指定してください",
|
||||
"login_form_err_invalid_email": "メールアドレスが無効です",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "スタックを解除",
|
||||
"wifi_name": "Wi-Fiの名前(SSID)",
|
||||
"your_wifi_name": "Wi-Fiの名前(SSID)"
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "뒤로",
|
||||
"login_form_button_text": "로그인",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "서버 엔드포인트 URL",
|
||||
"login_form_err_http": "http:// 또는 https://로 시작해야 합니다.",
|
||||
"login_form_err_invalid_email": "유효하지 않은 이메일",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Atpakaļ",
|
||||
"login_form_button_text": "Pieteikties",
|
||||
"login_form_email_hint": "jūsuepasts@email.com",
|
||||
"login_form_endpoint_hint": "http://jūsu-servera-ip:ports",
|
||||
"login_form_endpoint_hint": "http://jūsu-servera-ip:ports/api",
|
||||
"login_form_endpoint_url": "Servera Galapunkta URL",
|
||||
"login_form_err_http": "Lūdzu norādiet http:// vai https://",
|
||||
"login_form_err_invalid_email": "Nederīgs e-pasts",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Tilbake",
|
||||
"login_form_button_text": "Logg inn",
|
||||
"login_form_email_hint": "dinepost@epost.no",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Serverendepunkt-URL",
|
||||
"login_form_err_http": "Vennligst spesifiser http:// eller https://",
|
||||
"login_form_err_invalid_email": "Ugyldig e-postadresse",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Terug",
|
||||
"login_form_button_text": "Inloggen",
|
||||
"login_form_email_hint": "jouwemail@email.nl",
|
||||
"login_form_endpoint_hint": "http://jouw-server-ip:poort",
|
||||
"login_form_endpoint_hint": "http://jouw-server-ip:poort/api",
|
||||
"login_form_endpoint_url": "Server-URL",
|
||||
"login_form_err_http": "Voer http:// of https:// in",
|
||||
"login_form_err_invalid_email": "Ongeldig e-mailadres",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Cofnij",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "twojmail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-twojego-serwera:port",
|
||||
"login_form_endpoint_hint": "http://ip-twojego-serwera:port/api",
|
||||
"login_form_endpoint_url": "URL Serwera",
|
||||
"login_form_err_http": "Proszę określić http:// lub https://",
|
||||
"login_form_err_invalid_email": "Niepoprawny Email",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"exif_bottom_sheet_location": "LOCALIZAÇÃO",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "E-mail inválido",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Voltar",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "seuemail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-do-seu-servidor:porta",
|
||||
"login_form_endpoint_hint": "http://ip-do-seu-servidor:porta/api",
|
||||
"login_form_endpoint_url": "URL do servidor",
|
||||
"login_form_err_http": "Por favor especifique http:// ou https://",
|
||||
"login_form_err_invalid_email": "Email Inválido",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Înapoi",
|
||||
"login_form_button_text": "Conectare",
|
||||
"login_form_email_hint": "email-ultau@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-server:port",
|
||||
"login_form_endpoint_hint": "http://ip-server:port/api",
|
||||
"login_form_endpoint_url": "URL-ul destinației sever-ului",
|
||||
"login_form_err_http": "Te rugăm specifică http:// sau https://",
|
||||
"login_form_err_invalid_email": "Email invalid",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Назад",
|
||||
"login_form_button_text": "Войти",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "URL-aдрес сервера",
|
||||
"login_form_err_http": "Пожалуйста, укажите протокол http:// или https://",
|
||||
"login_form_err_invalid_email": "Некорректный адрес электронной почты",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Späť",
|
||||
"login_form_button_text": "Prihlásiť sa",
|
||||
"login_form_email_hint": "tvojmail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-tvojho-servera:port",
|
||||
"login_form_endpoint_hint": "http://ip-tvojho-servera:port/api",
|
||||
"login_form_endpoint_url": "URL adresa servera",
|
||||
"login_form_err_http": "Prosím, uveďte http:// alebo https://",
|
||||
"login_form_err_invalid_email": "Neplatný e-mail",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Nazaj",
|
||||
"login_form_button_text": "Prijava",
|
||||
"login_form_email_hint": "vašemail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-vašega-strežnika:vrata",
|
||||
"login_form_endpoint_hint": "http://ip-vašega-strežnika:vrata/api",
|
||||
"login_form_endpoint_url": "URL končne točke strežnika",
|
||||
"login_form_err_http": "Navedi http:// ali https://",
|
||||
"login_form_err_invalid_email": "Neveljaven e-poštni naslov",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Prijavi se",
|
||||
"login_form_email_hint": "vašemail@email.com",
|
||||
"login_form_endpoint_hint": "http://ip-vašeg-servera:port",
|
||||
"login_form_endpoint_hint": "http://ip-vašeg-servera:port/api",
|
||||
"login_form_endpoint_url": "URL Servera",
|
||||
"login_form_err_http": "Dopiši http:// ili https://",
|
||||
"login_form_err_invalid_email": "Nevažeći Email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Back",
|
||||
"login_form_button_text": "Login",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Please specify http:// or https://",
|
||||
"login_form_err_invalid_email": "Invalid Email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Bakåt",
|
||||
"login_form_button_text": "Logga in",
|
||||
"login_form_email_hint": "din.email@email.com",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_err_http": "Var god ange http:// eller https://",
|
||||
"login_form_err_invalid_email": "Ogiltig email",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "กลับ",
|
||||
"login_form_button_text": "เข้าสู่ระบบ",
|
||||
"login_form_email_hint": "อีเมลคุณ@อีเมล.com",
|
||||
"login_form_endpoint_hint": "http://ไอพีเชอร์ฟเวอร์คุณ:พอร์ต",
|
||||
"login_form_endpoint_hint": "http://ไอพีเชอร์ฟเวอร์คุณ:พอร์ต/api",
|
||||
"login_form_endpoint_url": "URL ปลายทางของเซิร์ฟเวอร์",
|
||||
"login_form_err_http": "โปรดระบุ http:// หรือ https://",
|
||||
"login_form_err_invalid_email": "อีเมลไม่ถูกต้อง",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Geri",
|
||||
"login_form_button_text": "Giriş",
|
||||
"login_form_email_hint": "mail@adresiniz.com",
|
||||
"login_form_endpoint_hint": "http://sunucu-ip:port",
|
||||
"login_form_endpoint_hint": "http://sunucu-ip:port/api",
|
||||
"login_form_endpoint_url": "Sunucu Uç Nokta URL",
|
||||
"login_form_err_http": "Lütfen http:// veya https:// olarak belirtin",
|
||||
"login_form_err_invalid_email": "Geçersiz E-posta",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Назад",
|
||||
"login_form_button_text": "Увійти",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||
"login_form_endpoint_url": "Адреса точки досупу на сервері",
|
||||
"login_form_err_http": "Вкажіть http:// або https://",
|
||||
"login_form_err_invalid_email": "Хибний імейл",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "Quay lại",
|
||||
"login_form_button_text": "Đăng nhập",
|
||||
"login_form_email_hint": "emailcuaban@email.com",
|
||||
"login_form_endpoint_hint": "http://địa-chỉ-ip-máy-chủ-bạn:cổng",
|
||||
"login_form_endpoint_hint": "http://địa-chỉ-ip-máy-chủ-bạn:cổng/api",
|
||||
"login_form_endpoint_url": "Địa chỉ máy chủ",
|
||||
"login_form_err_http": "Vui lòng xác định http:// hoặc https://",
|
||||
"login_form_err_invalid_email": "Email không hợp lệ",
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "后退",
|
||||
"login_form_button_text": "登录",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://您的服务器地址:端口",
|
||||
"login_form_endpoint_hint": "http(s)://您的服务器地址:端口/api",
|
||||
"login_form_endpoint_url": "服务器链接地址",
|
||||
"login_form_err_http": "请注明 http:// 或 https://",
|
||||
"login_form_err_invalid_email": "无效的电子邮箱",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "取消堆叠",
|
||||
"wifi_name": "Wi-Fi 名称",
|
||||
"your_wifi_name": "您的 Wi-Fi 名称"
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "后退",
|
||||
"login_form_button_text": "登录",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://您的服务器地址:端口",
|
||||
"login_form_endpoint_hint": "http(s)://您的服务器地址:端口/api",
|
||||
"login_form_endpoint_url": "服务器链接地址",
|
||||
"login_form_err_http": "请注明 http:// 或 https://",
|
||||
"login_form_err_invalid_email": "无效的电子邮箱",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "取消堆叠",
|
||||
"wifi_name": "Wi-Fi 名称",
|
||||
"your_wifi_name": "您的 Wi-Fi 名称"
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@
|
||||
"login_form_back_button_text": "後退",
|
||||
"login_form_button_text": "登入",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://您的伺服器地址:端口",
|
||||
"login_form_endpoint_hint": "http(s)://您的伺服器地址:端口/api",
|
||||
"login_form_endpoint_url": "伺服器鏈接地址",
|
||||
"login_form_err_http": "請注明 http:// 或 https://",
|
||||
"login_form_err_invalid_email": "電郵無效",
|
||||
@@ -670,4 +670,4 @@
|
||||
"viewer_unstack": "取消堆疊",
|
||||
"wifi_name": "WiFi Name",
|
||||
"your_wifi_name": "Your WiFi name"
|
||||
}
|
||||
}
|
||||
@@ -541,7 +541,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 187;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -685,7 +685,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 187;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -715,7 +715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 187;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -748,7 +748,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -791,7 +791,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -831,7 +831,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.125.1</string>
|
||||
<string>1.124.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -93,7 +93,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>189</string>
|
||||
<string>187</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
7
mobile/lib/interfaces/archive.interface.dart
Normal file
7
mobile/lib/interfaces/archive.interface.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
abstract interface class IArchiveRepository implements IDatabaseRepository {
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> getTimelineQuery(int userIsarId);
|
||||
}
|
||||
@@ -262,11 +262,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
|
||||
PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) {
|
||||
var newAsset = loadAsset(index);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(currentAssetProvider.notifier).set(newAsset);
|
||||
});
|
||||
|
||||
final stackId = newAsset.stackId;
|
||||
if (stackId != null && currentIndex.value == index) {
|
||||
final stackElements =
|
||||
|
||||
@@ -83,18 +83,11 @@ class PhotosPage extends HookConsumerWidget {
|
||||
|
||||
Future<void> refreshAssets() async {
|
||||
final fullRefresh = refreshCount.value > 0;
|
||||
|
||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||
if (fullRefresh) {
|
||||
Future.wait([
|
||||
ref.read(assetProvider.notifier).getAllAsset(clear: true),
|
||||
ref.read(albumProvider.notifier).refreshRemoteAlbums(),
|
||||
]);
|
||||
|
||||
// refresh was forced: user requested another refresh within 2 seconds
|
||||
refreshCount.value = 0;
|
||||
} else {
|
||||
await ref.read(assetProvider.notifier).getAllAsset(clear: false);
|
||||
|
||||
refreshCount.value++;
|
||||
// set counter back to 0 if user does not request refresh again
|
||||
Timer(const Duration(seconds: 4), () => refreshCount.value = 0);
|
||||
|
||||
25
mobile/lib/repositories/archive.repository.dart
Normal file
25
mobile/lib/repositories/archive.repository.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/archive.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final archiveRepositoryProvider =
|
||||
Provider((ref) => ArchiveRepository(ref.watch(dbProvider)));
|
||||
|
||||
class ArchiveRepository extends DatabaseRepository
|
||||
implements IArchiveRepository {
|
||||
ArchiveRepository(super.db);
|
||||
|
||||
@override
|
||||
QueryBuilder<Asset, Asset, QAfterSortBy> getTimelineQuery(int userIsarId) {
|
||||
return db.assets
|
||||
.where()
|
||||
.ownerIdEqualToAnyChecksum(userIsarId)
|
||||
.filter()
|
||||
.isArchivedEqualTo(true)
|
||||
.isTrashedEqualTo(false)
|
||||
.sortByFileCreatedAtDesc();
|
||||
}
|
||||
}
|
||||
15
mobile/lib/services/archive.service.dart
Normal file
15
mobile/lib/services/archive.service.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/repositories/archive.repository.dart';
|
||||
|
||||
final archiveServiceProvider = Provider((ref) {
|
||||
return ArchiveService(ref.watch(archiveRepositoryProvider));
|
||||
});
|
||||
|
||||
class ArchiveService {
|
||||
final ArchiveRepository repository;
|
||||
ArchiveService(this.repository);
|
||||
|
||||
getTimelineQuery(int userIsarId) {
|
||||
return repository.getTimelineQuery(userIsarId);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
||||
final localEndpoint = await _showEditDialog(
|
||||
context,
|
||||
"server_endpoint".tr(),
|
||||
"http://local-ip:2283",
|
||||
"http://local-ip:2283/api",
|
||||
localEndpointText.value,
|
||||
);
|
||||
|
||||
@@ -212,7 +212,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
||||
leading: const Icon(Icons.lan_rounded),
|
||||
title: Text("server_endpoint".tr()),
|
||||
subtitle: localEndpointText.value.isEmpty
|
||||
? const Text("http://local-ip:2283")
|
||||
? const Text("http://local-ip:2283/api")
|
||||
: Text(
|
||||
localEndpointText.value,
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['pages'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -122,4 +118,72 @@ void main() {
|
||||
captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured;
|
||||
expect(captured.first, emptyTextSearch);
|
||||
});
|
||||
|
||||
// COME BACK LATER
|
||||
// testWidgets('contextual search with text combined with media type',
|
||||
// (tester) async {
|
||||
// await tester.pumpConsumerWidget(
|
||||
// const SearchPage(),
|
||||
// overrides: overrides,
|
||||
// );
|
||||
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// expect(
|
||||
// find.byIcon(Icons.abc_rounded),
|
||||
// findsOneWidget,
|
||||
// reason: 'Should have contextual search icon',
|
||||
// );
|
||||
|
||||
// final searchField = find.byKey(const Key('search_text_field'));
|
||||
// expect(searchField, findsOneWidget);
|
||||
|
||||
// await tester.enterText(searchField, 'test');
|
||||
// await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||
|
||||
// var captured = verify(
|
||||
// () => mockSearchApi.searchSmart(captureAny()),
|
||||
// ).captured;
|
||||
|
||||
// expect(
|
||||
// captured.first,
|
||||
// isA<SmartSearchDto>().having((s) => s.query, 'query', 'test'),
|
||||
// );
|
||||
|
||||
// await tester.dragUntilVisible(
|
||||
// find.byKey(const Key('media_type_chip')),
|
||||
// find.byKey(const Key('search_filter_chip_list')),
|
||||
// const Offset(-100, 0),
|
||||
// );
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.tap(find.byKey(const Key('media_type_chip')));
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.tap(find.byKey(const Key('search_filter_media_type_image')));
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// await tester.tap(find.byKey(const Key('search_filter_apply')));
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured;
|
||||
|
||||
// expect(
|
||||
// captured.first,
|
||||
// isA<SmartSearchDto>()
|
||||
// .having((s) => s.query, 'query', 'test')
|
||||
// .having((s) => s.type, 'type', AssetTypeEnum.IMAGE),
|
||||
// );
|
||||
|
||||
// await tester.enterText(searchField, '');
|
||||
// await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||
|
||||
// captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured;
|
||||
// expect(
|
||||
// captured.first,
|
||||
// isA<MetadataSearchDto>()
|
||||
// .having((s) => s.originalFileName, 'originalFileName', null)
|
||||
// .having((s) => s.type, 'type', AssetTypeEnum.IMAGE),
|
||||
// );
|
||||
// });
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { entities } from 'src/entities';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||
@@ -21,7 +22,7 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
|
||||
import { providers, repositories } from 'src/repositories';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { teardownTelemetry } from 'src/repositories/telemetry.repository';
|
||||
import { services } from 'src/services';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { DatabaseService } from 'src/services/database.service';
|
||||
@@ -66,7 +67,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
logger: LoggingRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
@Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository,
|
||||
) {
|
||||
logger.setAppName(this.worker);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { EmailTemplate } from 'src/interfaces/notification.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { EmailTemplate } from 'src/repositories/notification.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
|
||||
@ApiTags('Notifications')
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
import { ImmichButton } from 'src/emails/components/button.component';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { AlbumInviteEmailProps } from 'src/repositories/notification.repository';
|
||||
import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface';
|
||||
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
|
||||
|
||||
export const AlbumInviteEmail = ({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
import { ImmichButton } from 'src/emails/components/button.component';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { AlbumUpdateEmailProps } from 'src/repositories/notification.repository';
|
||||
import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface';
|
||||
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
|
||||
|
||||
export const AlbumUpdateEmail = ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Row, Text } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { TestEmailProps } from 'src/repositories/notification.repository';
|
||||
import { TestEmailProps } from 'src/interfaces/notification.interface';
|
||||
|
||||
export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => (
|
||||
<ImmichLayout preview="This is a test email from Immich.">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Link, Section, Text } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
import { ImmichButton } from 'src/emails/components/button.component';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { WelcomeEmailProps } from 'src/repositories/notification.repository';
|
||||
import { WelcomeEmailProps } from 'src/interfaces/notification.interface';
|
||||
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
|
||||
|
||||
export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => {
|
||||
|
||||
18
server/src/interfaces/album-user.interface.ts
Normal file
18
server/src/interfaces/album-user.interface.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Insertable, Selectable, Updateable } from 'kysely';
|
||||
import { AlbumsSharedUsersUsers } from 'src/db';
|
||||
|
||||
export const IAlbumUserRepository = 'IAlbumUserRepository';
|
||||
|
||||
export type AlbumPermissionId = {
|
||||
albumsId: string;
|
||||
usersId: string;
|
||||
};
|
||||
|
||||
export interface IAlbumUserRepository {
|
||||
create(albumUser: Insertable<AlbumsSharedUsersUsers>): Promise<Selectable<AlbumsSharedUsersUsers>>;
|
||||
update(
|
||||
id: AlbumPermissionId,
|
||||
albumPermission: Updateable<AlbumsSharedUsersUsers>,
|
||||
): Promise<Selectable<AlbumsSharedUsersUsers>>;
|
||||
delete(id: AlbumPermissionId): Promise<void>;
|
||||
}
|
||||
20
server/src/interfaces/cron.interface.ts
Normal file
20
server/src/interfaces/cron.interface.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const ICronRepository = 'ICronRepository';
|
||||
|
||||
type CronBase = {
|
||||
name: string;
|
||||
start?: boolean;
|
||||
};
|
||||
|
||||
export type CronCreate = CronBase & {
|
||||
expression: string;
|
||||
onTick: () => void;
|
||||
};
|
||||
|
||||
export type CronUpdate = CronBase & {
|
||||
expression?: string;
|
||||
};
|
||||
|
||||
export interface ICronRepository {
|
||||
create(cron: CronCreate): void;
|
||||
update(cron: CronUpdate): void;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { EmailImageAttachment } from 'src/repositories/notification.repository';
|
||||
import { EmailImageAttachment } from 'src/interfaces/notification.interface';
|
||||
|
||||
export enum QueueName {
|
||||
THUMBNAIL_GENERATION = 'thumbnailGeneration',
|
||||
|
||||
31
server/src/interfaces/map.interface.ts
Normal file
31
server/src/interfaces/map.interface.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const IMapRepository = 'IMapRepository';
|
||||
|
||||
export interface MapMarkerSearchOptions {
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
fileCreatedBefore?: Date;
|
||||
fileCreatedAfter?: Date;
|
||||
}
|
||||
|
||||
export interface GeoPoint {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ReverseGeocodeResult {
|
||||
country: string | null;
|
||||
state: string | null;
|
||||
city: string | null;
|
||||
}
|
||||
|
||||
export interface MapMarker extends ReverseGeocodeResult {
|
||||
id: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
|
||||
export interface IMapRepository {
|
||||
init(): Promise<void>;
|
||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
}
|
||||
71
server/src/interfaces/metadata.interface.ts
Normal file
71
server/src/interfaces/metadata.interface.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { BinaryField, Tags } from 'exiftool-vendored';
|
||||
|
||||
export const IMetadataRepository = 'IMetadataRepository';
|
||||
|
||||
export interface ExifDuration {
|
||||
Value: number;
|
||||
Scale?: number;
|
||||
}
|
||||
|
||||
type StringOrNumber = string | number;
|
||||
|
||||
type TagsWithWrongTypes =
|
||||
| 'FocalLength'
|
||||
| 'Duration'
|
||||
| 'Description'
|
||||
| 'ImageDescription'
|
||||
| 'RegionInfo'
|
||||
| 'TagsList'
|
||||
| 'Keywords'
|
||||
| 'HierarchicalSubject'
|
||||
| 'ISO';
|
||||
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
||||
ContentIdentifier?: string;
|
||||
MotionPhoto?: number;
|
||||
MotionPhotoVersion?: number;
|
||||
MotionPhotoPresentationTimestampUs?: number;
|
||||
MediaGroupUUID?: string;
|
||||
ImagePixelDepth?: string;
|
||||
FocalLength?: number;
|
||||
Duration?: number | string | ExifDuration;
|
||||
EmbeddedVideoType?: string;
|
||||
EmbeddedVideoFile?: BinaryField;
|
||||
MotionPhotoVideo?: BinaryField;
|
||||
TagsList?: StringOrNumber[];
|
||||
HierarchicalSubject?: StringOrNumber[];
|
||||
Keywords?: StringOrNumber | StringOrNumber[];
|
||||
ISO?: number | number[];
|
||||
|
||||
// Type is wrong, can also be number.
|
||||
Description?: StringOrNumber;
|
||||
ImageDescription?: StringOrNumber;
|
||||
|
||||
// Extended properties for image regions, such as faces
|
||||
RegionInfo?: {
|
||||
AppliedToDimensions: {
|
||||
W: number;
|
||||
H: number;
|
||||
Unit: string;
|
||||
};
|
||||
RegionList: {
|
||||
Area: {
|
||||
// (X,Y) // center of the rectangle
|
||||
X: number;
|
||||
Y: number;
|
||||
W: number;
|
||||
H: number;
|
||||
Unit: string;
|
||||
};
|
||||
Rotation?: number;
|
||||
Type?: string;
|
||||
Name?: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IMetadataRepository {
|
||||
teardown(): Promise<void>;
|
||||
readTags(path: string): Promise<ImmichTags>;
|
||||
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
||||
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
||||
}
|
||||
101
server/src/interfaces/notification.interface.ts
Normal file
101
server/src/interfaces/notification.interface.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export const INotificationRepository = 'INotificationRepository';
|
||||
|
||||
export type EmailImageAttachment = {
|
||||
filename: string;
|
||||
path: string;
|
||||
cid: string;
|
||||
};
|
||||
|
||||
export type SendEmailOptions = {
|
||||
from: string;
|
||||
to: string;
|
||||
replyTo?: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
imageAttachments?: EmailImageAttachment[];
|
||||
smtp: SmtpOptions;
|
||||
};
|
||||
|
||||
export type SmtpOptions = {
|
||||
host: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
ignoreCert?: boolean;
|
||||
};
|
||||
|
||||
export enum EmailTemplate {
|
||||
TEST_EMAIL = 'test',
|
||||
|
||||
// AUTH
|
||||
WELCOME = 'welcome',
|
||||
RESET_PASSWORD = 'reset-password',
|
||||
|
||||
// ALBUM
|
||||
ALBUM_INVITE = 'album-invite',
|
||||
ALBUM_UPDATE = 'album-update',
|
||||
}
|
||||
|
||||
interface BaseEmailProps {
|
||||
baseUrl: string;
|
||||
customTemplate?: string;
|
||||
}
|
||||
|
||||
export interface TestEmailProps extends BaseEmailProps {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface WelcomeEmailProps extends BaseEmailProps {
|
||||
displayName: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface AlbumInviteEmailProps extends BaseEmailProps {
|
||||
albumName: string;
|
||||
albumId: string;
|
||||
senderName: string;
|
||||
recipientName: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export interface AlbumUpdateEmailProps extends BaseEmailProps {
|
||||
albumName: string;
|
||||
albumId: string;
|
||||
recipientName: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export type EmailRenderRequest =
|
||||
| {
|
||||
template: EmailTemplate.TEST_EMAIL;
|
||||
data: TestEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.WELCOME;
|
||||
data: WelcomeEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_INVITE;
|
||||
data: AlbumInviteEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_UPDATE;
|
||||
data: AlbumUpdateEmailProps;
|
||||
customTemplate: string;
|
||||
};
|
||||
|
||||
export type SendEmailResponse = {
|
||||
messageId: string;
|
||||
response: any;
|
||||
};
|
||||
|
||||
export interface INotificationRepository {
|
||||
renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }>;
|
||||
sendEmail(options: SendEmailOptions): Promise<SendEmailResponse>;
|
||||
verifySmtp(options: SmtpOptions): Promise<true>;
|
||||
}
|
||||
22
server/src/interfaces/oauth.interface.ts
Normal file
22
server/src/interfaces/oauth.interface.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { UserinfoResponse } from 'openid-client';
|
||||
|
||||
export const IOAuthRepository = 'IOAuthRepository';
|
||||
|
||||
export type OAuthConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
profileSigningAlgorithm: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
};
|
||||
export type OAuthProfile = UserinfoResponse;
|
||||
|
||||
export interface IOAuthRepository {
|
||||
init(): void;
|
||||
authorize(config: OAuthConfig, redirectUrl: string): Promise<string>;
|
||||
getLogoutEndpoint(config: OAuthConfig): Promise<string | undefined>;
|
||||
getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise<OAuthProfile>;
|
||||
}
|
||||
24
server/src/interfaces/server-info.interface.ts
Normal file
24
server/src/interfaces/server-info.interface.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface GitHubRelease {
|
||||
id: number;
|
||||
url: string;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ServerBuildVersions {
|
||||
nodejs: string;
|
||||
ffmpeg: string;
|
||||
libvips: string;
|
||||
exiftool: string;
|
||||
imagemagick: string;
|
||||
}
|
||||
|
||||
export const IServerInfoRepository = 'IServerInfoRepository';
|
||||
|
||||
export interface IServerInfoRepository {
|
||||
getGitHubRelease(): Promise<GitHubRelease>;
|
||||
getBuildVersions(): Promise<ServerBuildVersions>;
|
||||
}
|
||||
23
server/src/interfaces/telemetry.interface.ts
Normal file
23
server/src/interfaces/telemetry.interface.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { MetricOptions } from '@opentelemetry/api';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
|
||||
export const ITelemetryRepository = 'ITelemetryRepository';
|
||||
|
||||
export interface MetricGroupOptions {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface IMetricGroupRepository {
|
||||
addToCounter(name: string, value: number, options?: MetricOptions): void;
|
||||
addToGauge(name: string, value: number, options?: MetricOptions): void;
|
||||
addToHistogram(name: string, value: number, options?: MetricOptions): void;
|
||||
configure(options: MetricGroupOptions): this;
|
||||
}
|
||||
|
||||
export interface ITelemetryRepository {
|
||||
setup(options: { repositories: ClassConstructor<unknown>[] }): void;
|
||||
api: IMetricGroupRepository;
|
||||
host: IMetricGroupRepository;
|
||||
jobs: IMetricGroupRepository;
|
||||
repo: IMetricGroupRepository;
|
||||
}
|
||||
8
server/src/interfaces/trash.interface.ts
Normal file
8
server/src/interfaces/trash.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const ITrashRepository = 'ITrashRepository';
|
||||
|
||||
export interface ITrashRepository {
|
||||
empty(userId: string): Promise<number>;
|
||||
restore(userId: string): Promise<number>;
|
||||
restoreAll(assetIds: string[]): Promise<number>;
|
||||
getDeletedIds(): AsyncIterableIterator<{ id: string }>;
|
||||
}
|
||||
9
server/src/interfaces/version-history.interface.ts
Normal file
9
server/src/interfaces/version-history.interface.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
|
||||
|
||||
export const IVersionHistoryRepository = 'IVersionHistoryRepository';
|
||||
|
||||
export interface IVersionHistoryRepository {
|
||||
create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity>;
|
||||
getAll(): Promise<VersionHistoryEntity[]>;
|
||||
getLatest(): Promise<VersionHistoryEntity | undefined>;
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUpdatedAtTriggers1737672307560 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
create function updated_at()
|
||||
returns trigger as $$
|
||||
begin
|
||||
new."updatedAt" = now();
|
||||
return new;
|
||||
end;
|
||||
$$ language 'plpgsql'`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger activity_updated_at
|
||||
before update on activity
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger albums_updated_at
|
||||
before update on albums
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger api_keys_updated_at
|
||||
before update on api_keys
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger asset_files_updated_at
|
||||
before update on asset_files
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger assets_updated_at
|
||||
before update on assets
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger libraries_updated_at
|
||||
before update on libraries
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger memories_updated_at
|
||||
before update on memories
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger partners_updated_at
|
||||
before update on partners
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger person_updated_at
|
||||
before update on person
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger sessions_updated_at
|
||||
before update on sessions
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger tags_updated_at
|
||||
before update on tags
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
create trigger users_updated_at
|
||||
before update on users
|
||||
for each row execute procedure updated_at()
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`drop trigger activity_updated_at on activity`);
|
||||
await queryRunner.query(`drop trigger albums_updated_at on albums`);
|
||||
await queryRunner.query(`drop trigger api_keys_updated_at on api_keys`);
|
||||
await queryRunner.query(`drop trigger asset_files_updated_at on asset_files`);
|
||||
await queryRunner.query(`drop trigger assets_updated_at on assets`);
|
||||
await queryRunner.query(`drop trigger libraries_updated_at on libraries`);
|
||||
await queryRunner.query(`drop trigger memories_updated_at on memories`);
|
||||
await queryRunner.query(`drop trigger partners_updated_at on partners`);
|
||||
await queryRunner.query(`drop trigger person_updated_at on person`);
|
||||
await queryRunner.query(`drop trigger sessions_updated_at on sessions`);
|
||||
await queryRunner.query(`drop trigger tags_updated_at on tags`);
|
||||
await queryRunner.query(`drop trigger users_updated_at on users`);
|
||||
await queryRunner.query(`drop function updated_at_trigger`);
|
||||
}
|
||||
}
|
||||
@@ -306,26 +306,17 @@ order by
|
||||
with
|
||||
"duplicates" as (
|
||||
select
|
||||
"assets"."duplicateId",
|
||||
jsonb_agg("asset") as "assets"
|
||||
"duplicateId",
|
||||
jsonb_agg("assets") as "assets"
|
||||
from
|
||||
"assets"
|
||||
left join lateral (
|
||||
select
|
||||
"assets".*,
|
||||
"exif" as "exifInfo"
|
||||
from
|
||||
"exif"
|
||||
where
|
||||
"exif"."assetId" = "assets"."id"
|
||||
) as "asset" on true
|
||||
where
|
||||
"assets"."ownerId" = $1::uuid
|
||||
and "assets"."duplicateId" is not null
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
"ownerId" = $1::uuid
|
||||
and "duplicateId" is not null
|
||||
and "deletedAt" is null
|
||||
and "isVisible" = $2
|
||||
group by
|
||||
"assets"."duplicateId"
|
||||
"duplicateId"
|
||||
),
|
||||
"unique" as (
|
||||
select
|
||||
|
||||
@@ -60,8 +60,6 @@ union all
|
||||
limit
|
||||
$14
|
||||
)
|
||||
limit
|
||||
$15
|
||||
|
||||
-- SearchRepository.searchSmart
|
||||
select
|
||||
|
||||
@@ -4,14 +4,10 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AlbumsSharedUsersUsers, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
|
||||
export type AlbumPermissionId = {
|
||||
albumsId: string;
|
||||
usersId: string;
|
||||
};
|
||||
import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AlbumUserRepository {
|
||||
export class AlbumUserRepository implements IAlbumUserRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] })
|
||||
@@ -20,7 +16,10 @@ export class AlbumUserRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] })
|
||||
update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable<AlbumsSharedUsersUsers>) {
|
||||
update(
|
||||
{ usersId, albumsId }: AlbumPermissionId,
|
||||
dto: Updateable<AlbumsSharedUsersUsers>,
|
||||
): Promise<Selectable<AlbumsSharedUsersUsers>> {
|
||||
return this.db
|
||||
.updateTable('albums_shared_users_users')
|
||||
.set(dto)
|
||||
|
||||
@@ -43,8 +43,8 @@ import {
|
||||
WithProperty,
|
||||
WithoutProperty,
|
||||
} from 'src/interfaces/asset.interface';
|
||||
import { MapMarker, MapMarkerSearchOptions } from 'src/interfaces/map.interface';
|
||||
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/interfaces/search.interface';
|
||||
import { MapMarker, MapMarkerSearchOptions } from 'src/repositories/map.repository';
|
||||
import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database';
|
||||
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||
|
||||
@@ -430,9 +430,9 @@ export class AssetRepository implements IAssetRepository {
|
||||
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | undefined> {
|
||||
const { ownerId, otherAssetId, livePhotoCID, type } = options;
|
||||
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select('assets.id')
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.where('id', '!=', asUuid(otherAssetId))
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
@@ -677,23 +677,13 @@ export class AssetRepository implements IAssetRepository {
|
||||
.with('duplicates', (qb) =>
|
||||
qb
|
||||
.selectFrom('assets')
|
||||
.leftJoinLateral(
|
||||
(qb) =>
|
||||
qb
|
||||
.selectFrom('exif')
|
||||
.selectAll('assets')
|
||||
.select((eb) => eb.table('exif').as('exifInfo'))
|
||||
.whereRef('exif.assetId', '=', 'assets.id')
|
||||
.as('asset'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select('assets.duplicateId')
|
||||
.select((eb) => eb.fn('jsonb_agg', [eb.table('asset')]).as('assets'))
|
||||
.where('assets.ownerId', '=', asUuid(userId))
|
||||
.where('assets.duplicateId', 'is not', null)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.groupBy('assets.duplicateId'),
|
||||
.select('duplicateId')
|
||||
.select((eb) => eb.fn<Assets[]>('jsonb_agg', [eb.table('assets')]).as('assets'))
|
||||
.where('ownerId', '=', asUuid(userId))
|
||||
.where('duplicateId', 'is not', null)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('isVisible', '=', true)
|
||||
.groupBy('duplicateId'),
|
||||
)
|
||||
.with('unique', (qb) =>
|
||||
qb
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { CronJob, CronTime } from 'cron';
|
||||
import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
type CronBase = {
|
||||
name: string;
|
||||
start?: boolean;
|
||||
};
|
||||
|
||||
export type CronCreate = CronBase & {
|
||||
expression: string;
|
||||
onTick: () => void;
|
||||
};
|
||||
|
||||
export type CronUpdate = CronBase & {
|
||||
expression?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CronRepository {
|
||||
export class CronRepository implements ICronRepository {
|
||||
constructor(
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
private logger: LoggingRepository,
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { INotificationRepository } from 'src/interfaces/notification.interface';
|
||||
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IProcessRepository } from 'src/interfaces/process.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
@@ -61,44 +71,44 @@ import { ViewRepository } from 'src/repositories/view-repository';
|
||||
export const repositories = [
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AlbumUserRepository,
|
||||
AuditRepository,
|
||||
ApiKeyRepository,
|
||||
ConfigRepository,
|
||||
CronRepository,
|
||||
LoggingRepository,
|
||||
MapRepository,
|
||||
MediaRepository,
|
||||
MemoryRepository,
|
||||
MetadataRepository,
|
||||
NotificationRepository,
|
||||
OAuthRepository,
|
||||
ServerInfoRepository,
|
||||
TelemetryRepository,
|
||||
TrashRepository,
|
||||
ViewRepository,
|
||||
VersionHistoryRepository,
|
||||
];
|
||||
|
||||
export const providers = [
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
{ provide: ICronRepository, useClass: CronRepository },
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||
{ provide: IEventRepository, useClass: EventRepository },
|
||||
{ provide: IJobRepository, useClass: JobRepository },
|
||||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||
{ provide: IMapRepository, useClass: MapRepository },
|
||||
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
||||
{ provide: IMoveRepository, useClass: MoveRepository },
|
||||
{ provide: INotificationRepository, useClass: NotificationRepository },
|
||||
{ provide: IOAuthRepository, useClass: OAuthRepository },
|
||||
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||
{ provide: IProcessRepository, useClass: ProcessRepository },
|
||||
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||
{ provide: ISessionRepository, useClass: SessionRepository },
|
||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||
{ provide: IStackRepository, useClass: StackRepository },
|
||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||
{ provide: ITagRepository, useClass: TagRepository },
|
||||
{ provide: ITelemetryRepository, useClass: TelemetryRepository },
|
||||
{ provide: ITrashRepository, useClass: TrashRepository },
|
||||
{ provide: IUserRepository, useClass: UserRepository },
|
||||
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
|
||||
];
|
||||
|
||||
@@ -11,41 +11,24 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
|
||||
import { AssetEntity, withExif } from 'src/entities/asset.entity';
|
||||
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
|
||||
import { LogLevel, SystemMetadataKey } from 'src/enum';
|
||||
import {
|
||||
GeoPoint,
|
||||
IMapRepository,
|
||||
MapMarker,
|
||||
MapMarkerSearchOptions,
|
||||
ReverseGeocodeResult,
|
||||
} from 'src/interfaces/map.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export interface MapMarkerSearchOptions {
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
fileCreatedBefore?: Date;
|
||||
fileCreatedAfter?: Date;
|
||||
}
|
||||
|
||||
export interface GeoPoint {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ReverseGeocodeResult {
|
||||
country: string | null;
|
||||
state: string | null;
|
||||
city: string | null;
|
||||
}
|
||||
|
||||
export interface MapMarker extends ReverseGeocodeResult {
|
||||
id: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
|
||||
interface MapDB extends DB {
|
||||
geodata_places_tmp: GeodataPlaces;
|
||||
naturalearth_countries_tmp: NaturalearthCountries;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MapRepository {
|
||||
export class MapRepository implements IMapRepository {
|
||||
constructor(
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
|
||||
|
||||
@@ -1,72 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
||||
import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
||||
import geotz from 'geo-tz';
|
||||
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
interface ExifDuration {
|
||||
Value: number;
|
||||
Scale?: number;
|
||||
}
|
||||
|
||||
type StringOrNumber = string | number;
|
||||
|
||||
type TagsWithWrongTypes =
|
||||
| 'FocalLength'
|
||||
| 'Duration'
|
||||
| 'Description'
|
||||
| 'ImageDescription'
|
||||
| 'RegionInfo'
|
||||
| 'TagsList'
|
||||
| 'Keywords'
|
||||
| 'HierarchicalSubject'
|
||||
| 'ISO';
|
||||
|
||||
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
||||
ContentIdentifier?: string;
|
||||
MotionPhoto?: number;
|
||||
MotionPhotoVersion?: number;
|
||||
MotionPhotoPresentationTimestampUs?: number;
|
||||
MediaGroupUUID?: string;
|
||||
ImagePixelDepth?: string;
|
||||
FocalLength?: number;
|
||||
Duration?: number | string | ExifDuration;
|
||||
EmbeddedVideoType?: string;
|
||||
EmbeddedVideoFile?: BinaryField;
|
||||
MotionPhotoVideo?: BinaryField;
|
||||
TagsList?: StringOrNumber[];
|
||||
HierarchicalSubject?: StringOrNumber[];
|
||||
Keywords?: StringOrNumber | StringOrNumber[];
|
||||
ISO?: number | number[];
|
||||
|
||||
// Type is wrong, can also be number.
|
||||
Description?: StringOrNumber;
|
||||
ImageDescription?: StringOrNumber;
|
||||
|
||||
// Extended properties for image regions, such as faces
|
||||
RegionInfo?: {
|
||||
AppliedToDimensions: {
|
||||
W: number;
|
||||
H: number;
|
||||
Unit: string;
|
||||
};
|
||||
RegionList: {
|
||||
Area: {
|
||||
// (X,Y) // center of the rectangle
|
||||
X: number;
|
||||
Y: number;
|
||||
W: number;
|
||||
H: number;
|
||||
Unit: string;
|
||||
};
|
||||
Rotation?: number;
|
||||
Type?: string;
|
||||
Name?: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MetadataRepository {
|
||||
export class MetadataRepository implements IMetadataRepository {
|
||||
private exiftool = new ExifTool({
|
||||
defaultVideosToUTC: true,
|
||||
backfillTimezones: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EmailRenderRequest, EmailTemplate } from 'src/interfaces/notification.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
@@ -6,104 +6,18 @@ import { AlbumInviteEmail } from 'src/emails/album-invite.email';
|
||||
import { AlbumUpdateEmail } from 'src/emails/album-update.email';
|
||||
import { TestEmail } from 'src/emails/test.email';
|
||||
import { WelcomeEmail } from 'src/emails/welcome.email';
|
||||
import {
|
||||
EmailRenderRequest,
|
||||
EmailTemplate,
|
||||
INotificationRepository,
|
||||
SendEmailOptions,
|
||||
SendEmailResponse,
|
||||
SmtpOptions,
|
||||
} from 'src/interfaces/notification.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export type EmailImageAttachment = {
|
||||
filename: string;
|
||||
path: string;
|
||||
cid: string;
|
||||
};
|
||||
|
||||
export type SendEmailOptions = {
|
||||
from: string;
|
||||
to: string;
|
||||
replyTo?: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
imageAttachments?: EmailImageAttachment[];
|
||||
smtp: SmtpOptions;
|
||||
};
|
||||
|
||||
export type SmtpOptions = {
|
||||
host: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
ignoreCert?: boolean;
|
||||
};
|
||||
|
||||
export enum EmailTemplate {
|
||||
TEST_EMAIL = 'test',
|
||||
|
||||
// AUTH
|
||||
WELCOME = 'welcome',
|
||||
RESET_PASSWORD = 'reset-password',
|
||||
|
||||
// ALBUM
|
||||
ALBUM_INVITE = 'album-invite',
|
||||
ALBUM_UPDATE = 'album-update',
|
||||
}
|
||||
|
||||
interface BaseEmailProps {
|
||||
baseUrl: string;
|
||||
customTemplate?: string;
|
||||
}
|
||||
|
||||
export interface TestEmailProps extends BaseEmailProps {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface WelcomeEmailProps extends BaseEmailProps {
|
||||
displayName: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface AlbumInviteEmailProps extends BaseEmailProps {
|
||||
albumName: string;
|
||||
albumId: string;
|
||||
senderName: string;
|
||||
recipientName: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export interface AlbumUpdateEmailProps extends BaseEmailProps {
|
||||
albumName: string;
|
||||
albumId: string;
|
||||
recipientName: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export type EmailRenderRequest =
|
||||
| {
|
||||
template: EmailTemplate.TEST_EMAIL;
|
||||
data: TestEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.WELCOME;
|
||||
data: WelcomeEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_INVITE;
|
||||
data: AlbumInviteEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_UPDATE;
|
||||
data: AlbumUpdateEmailProps;
|
||||
customTemplate: string;
|
||||
};
|
||||
|
||||
export type SendEmailResponse = {
|
||||
messageId: string;
|
||||
response: any;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class NotificationRepository {
|
||||
export class NotificationRepository implements INotificationRepository {
|
||||
constructor(private logger: LoggingRepository) {
|
||||
this.logger.setContext(NotificationRepository.name);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { custom, generators, Issuer, UserinfoResponse } from 'openid-client';
|
||||
import { custom, generators, Issuer } from 'openid-client';
|
||||
import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export type OAuthConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
profileSigningAlgorithm: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
};
|
||||
export type OAuthProfile = UserinfoResponse;
|
||||
|
||||
@Injectable()
|
||||
export class OAuthRepository {
|
||||
export class OAuthRepository implements IOAuthRepository {
|
||||
constructor(private logger: LoggingRepository) {
|
||||
this.logger.setContext(OAuthRepository.name);
|
||||
}
|
||||
|
||||
@@ -133,6 +133,10 @@ export class PersonRepository implements IPersonRepository {
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.orderBy('person.isHidden', 'asc')
|
||||
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
|
||||
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
|
||||
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
|
||||
.orderBy('person.createdAt')
|
||||
.having((eb) =>
|
||||
eb.or([
|
||||
eb('person.name', '!=', ''),
|
||||
@@ -157,13 +161,6 @@ export class PersonRepository implements IPersonRepository {
|
||||
),
|
||||
),
|
||||
)
|
||||
.$if(!options?.closestFaceAssetId, (qb) =>
|
||||
qb
|
||||
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
|
||||
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
|
||||
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
|
||||
.orderBy('person.createdAt'),
|
||||
)
|
||||
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.offset(pagination.skip ?? 0)
|
||||
.limit(pagination.take + 1)
|
||||
|
||||
@@ -69,13 +69,12 @@ export class SearchRepository implements ISearchRepository {
|
||||
},
|
||||
],
|
||||
})
|
||||
async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
|
||||
searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
|
||||
const uuid = randomUUID();
|
||||
const builder = searchAssetBuilder(this.db, options);
|
||||
const lessThan = builder.where('assets.id', '<', uuid).orderBy('assets.id').limit(size);
|
||||
const greaterThan = builder.where('assets.id', '>', uuid).orderBy('assets.id').limit(size);
|
||||
const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
|
||||
return rows as any as AssetEntity[];
|
||||
return sql`${lessThan} union all ${greaterThan}`.execute(this.db) as any as Promise<AssetEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
|
||||
@@ -4,27 +4,10 @@ import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export interface GitHubRelease {
|
||||
id: number;
|
||||
url: string;
|
||||
tag_name: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ServerBuildVersions {
|
||||
nodejs: string;
|
||||
ffmpeg: string;
|
||||
libvips: string;
|
||||
exiftool: string;
|
||||
imagemagick: string;
|
||||
}
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
const maybeFirstLine = async (command: string): Promise<string> => {
|
||||
try {
|
||||
@@ -51,7 +34,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoRepository {
|
||||
export class ServerInfoRepository implements IServerInfoRepository {
|
||||
constructor(
|
||||
private configRepository: ConfigRepository,
|
||||
private logger: LoggingRepository,
|
||||
|
||||
@@ -15,12 +15,11 @@ import { MetricService } from 'nestjs-otel';
|
||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ImmichTelemetry, MetadataKey } from 'src/enum';
|
||||
import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
type MetricGroupOptions = { enabled: boolean };
|
||||
|
||||
export class MetricGroupRepository {
|
||||
class MetricGroupRepository implements IMetricGroupRepository {
|
||||
private enabled = false;
|
||||
|
||||
constructor(private metricService: MetricService) {}
|
||||
@@ -87,7 +86,7 @@ export const teardownTelemetry = async () => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class TelemetryRepository {
|
||||
export class TelemetryRepository implements ITelemetryRepository {
|
||||
api: MetricGroupRepository;
|
||||
host: MetricGroupRepository;
|
||||
jobs: MetricGroupRepository;
|
||||
|
||||
@@ -3,8 +3,9 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetStatus } from 'src/enum';
|
||||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
|
||||
export class TrashRepository {
|
||||
export class TrashRepository implements ITrashRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
getDeletedIds(): AsyncIterableIterator<{ id: string }> {
|
||||
|
||||
@@ -3,23 +3,25 @@ import { Insertable, Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, VersionHistory } from 'src/db';
|
||||
import { GenerateSql } from 'src/decorators';
|
||||
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
|
||||
@Injectable()
|
||||
export class VersionHistoryRepository {
|
||||
export class VersionHistoryRepository implements IVersionHistoryRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql()
|
||||
getAll() {
|
||||
getAll(): Promise<VersionHistoryEntity[]> {
|
||||
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getLatest() {
|
||||
getLatest(): Promise<VersionHistoryEntity | undefined> {
|
||||
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ version: 'v1.123.0' }] })
|
||||
create(version: Insertable<VersionHistory>) {
|
||||
create(version: Insertable<VersionHistory>): Promise<VersionHistoryEntity> {
|
||||
return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { IAlbumUserRepository } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
@@ -5,12 +5,13 @@ import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType, Permission } from 'src/enum';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { IApiKeyRepository, IOAuthRepository } from 'src/types';
|
||||
import { IApiKeyRepository } from 'src/types';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sessionStub } from 'test/fixtures/session.stub';
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
|
||||
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
||||
import { OAuthProfile } from 'src/interfaces/oauth.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
|
||||
@@ -2,13 +2,14 @@ import { PassThrough } from 'node:stream';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { ImmichWorker, StorageFolder } from 'src/enum';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IProcessRepository } from 'src/interfaces/process.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { BackupService } from 'src/services/backup.service';
|
||||
import { IConfigRepository, ICronRepository } from 'src/types';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { mockSpawn, newTestService } from 'test/utils';
|
||||
import { describe, Mocked } from 'vitest';
|
||||
|
||||
@@ -6,44 +6,44 @@ import { SALT_ROUNDS } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { Users } from 'src/db';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { INotificationRepository } from 'src/interfaces/notification.interface';
|
||||
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IProcessRepository } from 'src/interfaces/process.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MapRepository } from 'src/repositories/map.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { TrashRepository } from 'src/repositories/trash.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getConfig, updateConfig } from 'src/utils/config';
|
||||
@@ -57,10 +57,10 @@ export class BaseService {
|
||||
protected activityRepository: ActivityRepository,
|
||||
protected auditRepository: AuditRepository,
|
||||
@Inject(IAlbumRepository) protected albumRepository: IAlbumRepository,
|
||||
protected albumUserRepository: AlbumUserRepository,
|
||||
@Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository,
|
||||
@Inject(IAssetRepository) protected assetRepository: IAssetRepository,
|
||||
protected configRepository: ConfigRepository,
|
||||
protected cronRepository: CronRepository,
|
||||
@Inject(ICronRepository) protected cronRepository: ICronRepository,
|
||||
@Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository,
|
||||
@Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository,
|
||||
@Inject(IEventRepository) protected eventRepository: IEventRepository,
|
||||
@@ -68,28 +68,28 @@ export class BaseService {
|
||||
protected keyRepository: ApiKeyRepository,
|
||||
@Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository,
|
||||
@Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository,
|
||||
protected mapRepository: MapRepository,
|
||||
@Inject(IMapRepository) protected mapRepository: IMapRepository,
|
||||
protected mediaRepository: MediaRepository,
|
||||
protected memoryRepository: MemoryRepository,
|
||||
protected metadataRepository: MetadataRepository,
|
||||
@Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository,
|
||||
@Inject(IMoveRepository) protected moveRepository: IMoveRepository,
|
||||
protected notificationRepository: NotificationRepository,
|
||||
protected oauthRepository: OAuthRepository,
|
||||
@Inject(INotificationRepository) protected notificationRepository: INotificationRepository,
|
||||
@Inject(IOAuthRepository) protected oauthRepository: IOAuthRepository,
|
||||
@Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository,
|
||||
@Inject(IPersonRepository) protected personRepository: IPersonRepository,
|
||||
@Inject(IProcessRepository) protected processRepository: IProcessRepository,
|
||||
@Inject(ISearchRepository) protected searchRepository: ISearchRepository,
|
||||
protected serverInfoRepository: ServerInfoRepository,
|
||||
@Inject(IServerInfoRepository) protected serverInfoRepository: IServerInfoRepository,
|
||||
@Inject(ISessionRepository) protected sessionRepository: ISessionRepository,
|
||||
@Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
|
||||
@Inject(IStackRepository) protected stackRepository: IStackRepository,
|
||||
@Inject(IStorageRepository) protected storageRepository: IStorageRepository,
|
||||
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ITagRepository) protected tagRepository: ITagRepository,
|
||||
protected telemetryRepository: TelemetryRepository,
|
||||
protected trashRepository: TrashRepository,
|
||||
@Inject(ITelemetryRepository) protected telemetryRepository: ITelemetryRepository,
|
||||
@Inject(ITrashRepository) protected trashRepository: ITrashRepository,
|
||||
@Inject(IUserRepository) protected userRepository: IUserRepository,
|
||||
protected versionRepository: VersionHistoryRepository,
|
||||
@Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository,
|
||||
protected viewRepository: ViewRepository,
|
||||
) {
|
||||
this.logger.setContext(this.constructor.name);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user