1
0
forked from Cutlery/immich

Compare commits

..

34 Commits

Author SHA1 Message Date
Fynn Petersen-Frey 14ce849f90 update generated Isar code 2023-11-13 18:52:16 +01:00
martabal 49502f3230 fix: tests 2023-11-13 17:44:06 +01:00
martabal fdf4f93208 fix: mobile 2023-11-13 17:12:32 +01:00
martabal 5d6bfa5a3e merge main 2023-11-13 17:11:46 +01:00
martabal 5c37b27fcf pr feedback 2023-11-13 17:01:48 +01:00
martabal 0ed5dc869a pr feedback 2023-11-13 17:00:29 +01:00
martabal ac9e2cd316 pr feedback 2023-11-13 16:46:30 +01:00
martabal af0f2f005b fix: e2e test 2023-11-13 12:41:20 +01:00
shalong-tanwen 543ff6f7fd conflict changes 2023-11-13 16:47:09 +05:30
martabal f249a3761b chore: regenerate api 2023-11-13 12:07:19 +01:00
martabal b96f04efec merge main 2023-11-13 11:59:47 +01:00
shalong-tanwen 1404b6441d conflict changes 2023-11-08 19:31:34 +05:30
martabal 91f8297a61 chore: regenerate api 2023-11-08 14:48:50 +01:00
martabal 97206faadb merge main 2023-11-08 14:47:58 +01:00
shalong-tanwen e3d6f7adb3 feat(mobile): user avatar colors 2023-11-06 01:30:22 +05:30
martabal 000e1f17c5 merge main 2023-11-05 18:33:30 +01:00
martabal ee4120c5f7 merge main 2023-11-05 18:32:07 +01:00
martabal 6faa597aaf fix: tests 2023-11-04 17:14:24 +01:00
martabal 21210ca297 fix: svelte file in correct folder 2023-11-04 15:47:09 +01:00
martabal 11f1ade8f2 pr feedback 2023-11-04 15:39:48 +01:00
martabal 40c1bfa27b merge main 2023-11-04 15:23:49 +01:00
martabal 6c2bf550bc chore: regenerate api 2023-11-04 15:22:37 +01:00
martabal f28c369c16 merge main 2023-11-04 15:21:58 +01:00
martabal fb9b854bf1 merge main 2023-11-04 15:21:33 +01:00
martabal fe9348e049 remove autoColor from UserAvatar 2023-11-01 22:57:55 +01:00
martabal 8a6af72588 fix: do not use fix height and width 2023-11-01 21:20:29 +01:00
martabal 610b03d16c pr feedback 2023-11-01 21:02:34 +01:00
martabal 3d6eadb595 fix: tests 2023-11-01 20:32:03 +01:00
martabal 68ebcf218d fix: tests 2023-11-01 20:23:11 +01:00
martabal 46d640b7a1 pr feedback 2023-11-01 20:13:21 +01:00
martabal 839cc4f3f8 fix: tests 2023-11-01 19:14:46 +01:00
martabal 4f77d06592 feat: random avatar color on user creation 2023-11-01 19:06:35 +01:00
martabal 8c1e782f8f fix: tests 2023-11-01 18:52:23 +01:00
martabal cda2ff3d1f feat: user avatar color 2023-11-01 18:22:49 +01:00
51 changed files with 309 additions and 687 deletions
+3 -3
View File
@@ -168,13 +168,13 @@ jobs:
poetry install --with dev
- name: Lint with ruff
run: |
poetry run ruff check --format=github app export
poetry run ruff check --format=github app
- name: Check black formatting
run: |
poetry run black --check app export
poetry run black --check app
- name: Run mypy type checking
run: |
poetry run mypy --install-types --non-interactive --strict app/ export/
poetry run mypy --install-types --non-interactive app/
- name: Run tests and coverage
run: |
poetry run pytest --cov app
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -2
View File
@@ -36,8 +36,7 @@ def deployed_app() -> TestClient:
@pytest.fixture(scope="session")
def responses() -> dict[str, Any]:
responses: dict[str, Any] = json.load(open("responses.json", "r"))
return responses
return json.load(open("responses.json", "r"))
@pytest.fixture(scope="session")
+1 -1
View File
@@ -7,7 +7,7 @@ from zipfile import BadZipFile
import orjson
from fastapi import FastAPI, Form, HTTPException, UploadFile
from fastapi.responses import ORJSONResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile # type: ignore
from starlette.formparsers import MultiPartParser
from app.models.base import InferenceModel
+4 -6
View File
@@ -8,7 +8,6 @@ from typing import Any
import onnxruntime as ort
from huggingface_hub import snapshot_download
from typing_extensions import Buffer
from ..config import get_cache_dir, get_hf_model_name, log, settings
from ..schemas import ModelType
@@ -140,12 +139,11 @@ class InferenceModel(ABC):
# HF deep copies configs, so we need to make session options picklable
class PicklableSessionOptions(ort.SessionOptions): # type: ignore[misc]
class PicklableSessionOptions(ort.SessionOptions):
def __getstate__(self) -> bytes:
return pickle.dumps([(attr, getattr(self, attr)) for attr in dir(self) if not callable(getattr(self, attr))])
def __setstate__(self, state: Buffer) -> None:
self.__init__() # type: ignore[misc]
attrs: list[tuple[str, Any]] = pickle.loads(state)
for attr, val in attrs:
def __setstate__(self, state: Any) -> None:
self.__init__() # type: ignore
for attr, val in pickle.loads(state):
setattr(self, attr, val)
+5 -5
View File
@@ -6,7 +6,7 @@ from aiocache.plugins import BasePlugin, TimingPlugin
from app.models import from_model_type
from ..schemas import ModelType, has_profiling
from ..schemas import ModelType
from .base import InferenceModel
@@ -50,20 +50,20 @@ class ModelCache:
key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}"
async with OptimisticLock(self.cache, key) as lock:
model: InferenceModel | None = await self.cache.get(key)
model = await self.cache.get(key)
if model is None:
model = from_model_type(model_type, model_name, **model_kwargs)
await lock.cas(model, ttl=self.ttl)
return model
async def get_profiling(self) -> dict[str, float] | None:
if not has_profiling(self.cache):
if not hasattr(self.cache, "profiling"):
return None
return self.cache.profiling
return self.cache.profiling # type: ignore
class RevalidationPlugin(BasePlugin): # type: ignore[misc]
class RevalidationPlugin(BasePlugin):
"""Revalidates cache item's TTL after cache hit."""
async def post_get(
+6 -8
View File
@@ -51,7 +51,7 @@ class BaseCLIPEncoder(InferenceModel):
provider_options=self.provider_options,
)
def _predict(self, image_or_text: Image.Image | str) -> ndarray_f32:
def _predict(self, image_or_text: Image.Image | str) -> list[float]:
if isinstance(image_or_text, bytes):
image_or_text = Image.open(BytesIO(image_or_text))
@@ -60,16 +60,16 @@ class BaseCLIPEncoder(InferenceModel):
if self.mode == "text":
raise TypeError("Cannot encode image as text-only model")
outputs: ndarray_f32 = self.vision_model.run(None, self.transform(image_or_text))[0][0]
outputs = self.vision_model.run(None, self.transform(image_or_text))
case str():
if self.mode == "vision":
raise TypeError("Cannot encode text as vision-only model")
outputs = self.text_model.run(None, self.tokenize(image_or_text))[0][0]
outputs = self.text_model.run(None, self.tokenize(image_or_text))
case _:
raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
return outputs
return outputs[0][0].tolist()
@abstractmethod
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
@@ -151,13 +151,11 @@ class OpenCLIPEncoder(BaseCLIPEncoder):
@cached_property
def model_cfg(self) -> dict[str, Any]:
model_cfg: dict[str, Any] = json.load(self.model_cfg_path.open())
return model_cfg
return json.load(self.model_cfg_path.open())
@cached_property
def preprocess_cfg(self) -> dict[str, Any]:
preprocess_cfg: dict[str, Any] = json.load(self.preprocess_cfg_path.open())
return preprocess_cfg
return json.load(self.preprocess_cfg_path.open())
class MCLIPEncoder(OpenCLIPEncoder):
@@ -8,7 +8,7 @@ from insightface.model_zoo import ArcFaceONNX, RetinaFace
from insightface.utils.face_align import norm_crop
from app.config import clean_name
from app.schemas import BoundingBox, Face, ModelType, ndarray_f32
from app.schemas import ModelType, ndarray_f32
from .base import InferenceModel
@@ -52,7 +52,7 @@ class FaceRecognizer(InferenceModel):
)
self.rec_model.prepare(ctx_id=0)
def _predict(self, image: ndarray_f32 | bytes) -> list[Face]:
def _predict(self, image: ndarray_f32 | bytes) -> list[dict[str, Any]]:
if isinstance(image, bytes):
image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR)
bboxes, kpss = self.det_model.detect(image)
@@ -67,20 +67,21 @@ class FaceRecognizer(InferenceModel):
height, width, _ = image.shape
for (x1, y1, x2, y2), score, kps in zip(bboxes, scores, kpss):
cropped_img = norm_crop(image, kps)
embedding: ndarray_f32 = self.rec_model.get_feat(cropped_img)[0]
face: Face = {
"imageWidth": width,
"imageHeight": height,
"boundingBox": {
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
},
"score": score,
"embedding": embedding,
}
results.append(face)
embedding = self.rec_model.get_feat(cropped_img)[0].tolist()
results.append(
{
"imageWidth": width,
"imageHeight": height,
"boundingBox": {
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
},
"score": score,
"embedding": embedding,
}
)
return results
@property
@@ -66,7 +66,7 @@ class ImageClassifier(InferenceModel):
def _predict(self, image: Image.Image | bytes) -> list[str]:
if isinstance(image, bytes):
image = Image.open(BytesIO(image))
predictions: list[dict[str, Any]] = self.model(image)
predictions: list[dict[str, Any]] = self.model(image) # type: ignore
tags = [tag for pred in predictions for tag in pred["label"].split(", ") if pred["score"] >= self.min_score]
return tags
+13 -19
View File
@@ -1,12 +1,17 @@
from enum import StrEnum
from typing import Any, Protocol, TypeAlias, TypedDict, TypeGuard
from typing import TypeAlias
import numpy as np
from pydantic import BaseModel
ndarray_f32: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
ndarray_i64: TypeAlias = np.ndarray[int, np.dtype[np.int64]]
ndarray_i32: TypeAlias = np.ndarray[int, np.dtype[np.int32]]
def to_lower_camel(string: str) -> str:
tokens = [token.capitalize() if i > 0 else token for i, token in enumerate(string.split("_"))]
return "".join(tokens)
class TextModelRequest(BaseModel):
text: str
class TextResponse(BaseModel):
@@ -17,7 +22,7 @@ class MessageResponse(BaseModel):
message: str
class BoundingBox(TypedDict):
class BoundingBox(BaseModel):
x1: int
y1: int
x2: int
@@ -30,17 +35,6 @@ class ModelType(StrEnum):
FACIAL_RECOGNITION = "facial-recognition"
class HasProfiling(Protocol):
profiling: dict[str, float]
class Face(TypedDict):
boundingBox: BoundingBox
embedding: ndarray_f32
imageWidth: int
imageHeight: int
score: float
def has_profiling(obj: Any) -> TypeGuard[HasProfiling]:
return hasattr(obj, "profiling") and type(obj.profiling) == dict
ndarray_f32: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
ndarray_i64: TypeAlias = np.ndarray[int, np.dtype[np.int64]]
ndarray_i32: TypeAlias = np.ndarray[int, np.dtype[np.int32]]
+9 -9
View File
@@ -75,9 +75,9 @@ class TestCLIP:
embedding = clip_encoder.predict(pil_image)
assert clip_encoder.mode == "vision"
assert isinstance(embedding, np.ndarray)
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
assert embedding.dtype == np.float32
assert isinstance(embedding, list)
assert len(embedding) == clip_model_cfg["embed_dim"]
assert all([isinstance(num, float) for num in embedding])
clip_encoder.vision_model.run.assert_called_once()
def test_basic_text(
@@ -97,9 +97,9 @@ class TestCLIP:
embedding = clip_encoder.predict("test search query")
assert clip_encoder.mode == "text"
assert isinstance(embedding, np.ndarray)
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
assert embedding.dtype == np.float32
assert isinstance(embedding, list)
assert len(embedding) == clip_model_cfg["embed_dim"]
assert all([isinstance(num, float) for num in embedding])
clip_encoder.text_model.run.assert_called_once()
@@ -133,9 +133,9 @@ class TestFaceRecognition:
for face in faces:
assert face["imageHeight"] == 800
assert face["imageWidth"] == 600
assert isinstance(face["embedding"], np.ndarray)
assert face["embedding"].shape[0] == 512
assert face["embedding"].dtype == np.float32
assert isinstance(face["embedding"], list)
assert len(face["embedding"]) == 512
assert all([isinstance(num, float) for num in face["embedding"]])
det_model.detect.assert_called_once()
assert rec_model.get_feat.call_count == num_faces
+4 -9
View File
@@ -1,7 +1,6 @@
import tempfile
import warnings
from dataclasses import dataclass, field
from math import e
from pathlib import Path
import open_clip
@@ -70,12 +69,10 @@ def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig,
output_path = Path(output_path)
def encode_image(image: torch.Tensor) -> torch.Tensor:
output = model.encode_image(image, normalize=True)
assert isinstance(output, torch.Tensor)
return output
return model.encode_image(image, normalize=True)
args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),)
traced = torch.jit.trace(encode_image, args) # type: ignore[no-untyped-call]
traced = torch.jit.trace(encode_image, args)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
@@ -94,12 +91,10 @@ def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, o
output_path = Path(output_path)
def encode_text(text: torch.Tensor) -> torch.Tensor:
output = model.encode_text(text, normalize=True)
assert isinstance(output, torch.Tensor)
return output
return model.encode_text(text, normalize=True)
args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),)
traced = torch.jit.trace(encode_text, args) # type: ignore[no-untyped-call]
traced = torch.jit.trace(encode_text, args)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.86.0"
version = "1.85.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 110,
"android.injected.version.name" => "1.86.0",
"android.injected.version.code" => 109,
"android.injected.version.name" => "1.85.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
-384
View File
@@ -1,384 +0,0 @@
{
"add_to_album_bottom_sheet_added": "Ajouté à {album}",
"add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
"advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des vignettes à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.",
"advanced_settings_prefer_remote_title": "Préférer les images externes",
"advanced_settings_self_signed_ssl_subtitle": "Permet d'ignorer la vérification du certificat SSL pour le point d'accès du serveur. Requis pour les certificats auto-signés.",
"advanced_settings_self_signed_ssl_title": "Autoriser les certificats SSL auto-signés",
"advanced_settings_tile_subtitle": "Paramètres d'utilisateur avancés",
"advanced_settings_tile_title": "Avancé",
"advanced_settings_troubleshooting_subtitle": "Activer des fonctions supplémentaires pour le dépannage",
"advanced_settings_troubleshooting_title": "Dépannage",
"album_info_card_backup_album_excluded": "EXCLUS",
"album_info_card_backup_album_included": "INCLUS",
"album_thumbnail_card_item": "1 élément",
"album_thumbnail_card_items": "{} éléments",
"album_thumbnail_card_shared": " · Partagé",
"album_thumbnail_owned": "Possédé",
"album_thumbnail_shared_by": "Partagé par {}",
"album_viewer_appbar_share_delete": "Supprimer l'album",
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
"album_viewer_appbar_share_err_remove": "Il y a des problèmes lors de la suppression des éléments de l'album",
"album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album",
"album_viewer_appbar_share_leave": "Quitter l'album",
"album_viewer_appbar_share_remove": "Retirer de l'album",
"album_viewer_appbar_share_to": "Partager à",
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
"all_people_page_title": "Personnes",
"all_videos_page_title": "Vidéos",
"app_bar_signout_dialog_content": "Êtes-vous sûr de vouloir vous déconnecter?",
"app_bar_signout_dialog_ok": "Oui",
"app_bar_signout_dialog_title": "Se déconnecter",
"archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique",
"asset_list_layout_settings_group_automatically": "Automatique",
"asset_list_layout_settings_group_by": "Grouper les éléments par",
"asset_list_layout_settings_group_by_month": "Mois",
"asset_list_layout_settings_group_by_month_day": "Mois + jour",
"asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos",
"asset_list_settings_title": "Grille de photos",
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
"backup_album_selection_page_select_albums": "Sélectionner les albums",
"backup_album_selection_page_selection_info": "Informations sur la sélection",
"backup_album_selection_page_total_assets": "Total des éléments uniques",
"backup_all": "Tout",
"backup_background_service_backup_failed_message": "Échec de la sauvegarde des éléments. Nouvelle tentative...",
"backup_background_service_connection_failed_message": "Impossible de se connecter au serveur. Nouvelle tentative...",
"backup_background_service_current_upload_notification": "Transfert {}",
"backup_background_service_default_notification": "Recherche de nouveaux éléments...",
"backup_background_service_error_title": "Erreur de sauvegarde",
"backup_background_service_in_progress_notification": "Sauvegarde de vos éléments...",
"backup_background_service_upload_failure_notification": "Impossible de transférer {}",
"backup_controller_page_albums": "Sauvegarder les albums",
"backup_controller_page_background_app_refresh_disabled_content": "Activez le rafraîchissement de l'application en arrière-plan dans Paramètres > Général > Rafraîchissement de l'application en arrière-plan afin d'utiliser la sauvegarde en arrière-plan.",
"backup_controller_page_background_app_refresh_disabled_title": "Rafraîchissement de l'application en arrière-plan désactivé",
"backup_controller_page_background_app_refresh_enable_button_text": "Aller aux paramètres",
"backup_controller_page_background_battery_info_link": "Montrez-moi comment",
"backup_controller_page_background_battery_info_message": "Pour une expérience optimale de la sauvegarde en arrière-plan, veuillez désactiver toute optimisation de la batterie limitant l'activité en arrière-plan pour Immich.\n\nÉtant donné que cela est spécifique à chaque appareil, veuillez consulter les informations requises pour le fabricant de votre appareil.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Optimisation de la batterie",
"backup_controller_page_background_charging": "Seulement pendant la charge",
"backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan",
"backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments d'actif: {}",
"backup_controller_page_background_description": "Activez le service d'arrière-plan pour sauvegarder automatiquement tous les nouveaux éléments sans avoir à ouvrir l'application.",
"backup_controller_page_background_is_off": "La sauvegarde automatique en arrière-plan est désactivée",
"backup_controller_page_background_is_on": "La sauvegarde automatique en arrière-plan est activée",
"backup_controller_page_background_turn_off": "Désactiver le service d'arrière-plan",
"backup_controller_page_background_turn_on": "Activer le service d'arrière-plan",
"backup_controller_page_background_wifi": "Uniquement sur WiFi",
"backup_controller_page_backup": "Sauvegardé",
"backup_controller_page_backup_selected": "Sélectionné: ",
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
"backup_controller_page_cancel": "Annuler",
"backup_controller_page_created": "Créé le: {}",
"backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.",
"backup_controller_page_excluded": "Exclus: ",
"backup_controller_page_failed": "Échec de l'opération ({})",
"backup_controller_page_filename": "Nom du fichier: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informations de sauvegarde",
"backup_controller_page_none_selected": "Aucune sélection",
"backup_controller_page_remainder": "Restant",
"backup_controller_page_remainder_sub": "Photos et albums restants à sauvegarder à partir de la sélection",
"backup_controller_page_select": "Sélectionner",
"backup_controller_page_server_storage": "Stockage du serveur",
"backup_controller_page_start_backup": "Démarrer la sauvegarde",
"backup_controller_page_status_off": "La sauvegarde est désactivée",
"backup_controller_page_status_on": "La sauvegarde est activée",
"backup_controller_page_storage_format": "{} de {} utilisé",
"backup_controller_page_to_backup": "Albums à sauvegarder",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés",
"backup_controller_page_turn_off": "Désactiver la sauvegarde",
"backup_controller_page_turn_on": "Activer la sauvegarde",
"backup_controller_page_uploading_file_info": "Transfert des informations du fichier",
"backup_err_only_album": "Impossible de retirer le seul album",
"backup_info_card_assets": "éléments",
"backup_manual_cancelled": "Annulé",
"backup_manual_failed": "Echec",
"backup_manual_in_progress": "Téléchargement déjà en cours. Essayez après un instant",
"backup_manual_success": "Succès ",
"backup_manual_title": "Statut du téléchargement ",
"cache_settings_album_thumbnails": "vignettes de la page bibliothèque ({} éléments)",
"cache_settings_clear_cache_button": "Effacer le cache",
"cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.",
"cache_settings_image_cache_size": "Taille du cache des images ({} éléments)",
"cache_settings_statistics_album": "vignettes de la bibliothèque",
"cache_settings_statistics_assets": "{} éléments ({})",
"cache_settings_statistics_full": "Images complètes",
"cache_settings_statistics_shared": "vignettes d'albums partagés",
"cache_settings_statistics_thumbnail": "vignettes",
"cache_settings_statistics_title": "Utilisation du cache",
"cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich",
"cache_settings_thumbnail_size": "Taille du cache des vignettes ({} éléments)",
"cache_settings_tile_subtitle": "Contrôler le comportement du stockage local",
"cache_settings_tile_title": "Stockage local",
"cache_settings_title": "Paramètres de mise en cache",
"change_password_form_confirm_password": "Confirmez le mot de passe",
"change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
"change_password_form_new_password": "Nouveau mot de passe",
"change_password_form_password_mismatch": "Les mots de passe ne correspondent pas",
"change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe",
"common_add_to_album": "Ajouter à l'album",
"common_change_password": "Modifier le mot de passe",
"common_create_new_album": "Créer un nouvel album",
"common_server_error": "Veuillez vérifier votre connexion réseau, vous assurer que le serveur est accessible et que les versions de l'application et du serveur sont compatibles.",
"common_shared": "Partagé",
"control_bottom_app_bar_add_to_album": "Ajouter à l'album",
"control_bottom_app_bar_album_info": "{} éléments",
"control_bottom_app_bar_album_info_shared": "{} éléments - Partagés",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Créer un nouvel album",
"control_bottom_app_bar_delete": "Supprimer",
"control_bottom_app_bar_favorite": "Favoris",
"control_bottom_app_bar_share": "Partager",
"control_bottom_app_bar_share_to": "Partager à",
"control_bottom_app_bar_stack": "Empiler",
"control_bottom_app_bar_unarchive": "Désarchiver",
"control_bottom_app_bar_upload": "Téléverser",
"create_album_page_untitled": "Sans titre",
"create_shared_album_page_create": "Créer",
"create_shared_album_page_share": "Partager",
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
"curated_location_page_title": "Places",
"curated_object_page_title": "Objets",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.",
"delete_dialog_cancel": "Annuler",
"delete_dialog_ok": "Supprimer",
"delete_dialog_title": "Supprimer définitivement",
"delete_shared_link_dialog_content": "Êtes-vous sûr de vouloir supprimer ce lien partagé?",
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
"description_input_hint_text": "Ajouter une description...",
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
"exif_bottom_sheet_description": "Ajouter une description...",
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
"experimental_settings_new_asset_list_subtitle": "En cours de développement",
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
"experimental_settings_subtitle": "Utilisez à vos dépends!",
"experimental_settings_title": "Expérimental",
"favorites_page_no_favorites": "Aucun élément favori n'a été trouvé",
"favorites_page_title": "Favoris",
"home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.",
"home_page_add_to_album_err_local": "Impossible d'ajouter des éléments locaux aux albums pour le moment, étape ignorée",
"home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.",
"home_page_archive_err_local": "Impossible d'archiver les ressources locales pour l'instant, étape ignorée",
"home_page_building_timeline": "Construction de la chronologie",
"home_page_favorite_err_local": "Impossible d'ajouter des éléments locaux aux favoris pour le moment, étape ignorée",
"home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
"home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
"image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
"image_viewer_page_state_provider_download_success": "Téléchargement réussi",
"image_viewer_page_state_provider_share_error": "Erreur de partage",
"library_page_albums": "Albums",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums sur l'appareil",
"library_page_favorites": "Favoris",
"library_page_new_album": "Nouvel album",
"library_page_sharing": "Partage",
"library_page_sort_created": "Créations les plus récentes",
"library_page_sort_last_modified": "Dernière modification",
"library_page_sort_most_recent_photo": "Photo la plus récente",
"library_page_sort_title": "Titre de l'album",
"login_disabled": "La connexion a été désactivée ",
"login_form_api_exception": "Erreur de l'API. Veuillez vérifier l'URL du serveur et et réessayer.",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
"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",
"login_form_err_invalid_url": "URL invalide",
"login_form_err_leading_whitespace": "Espace en début de ligne",
"login_form_err_trailing_whitespace": "Espace de fin de ligne",
"login_form_failed_get_oauth_server_config": "Erreur de connexion par OAuth, vérifiez l\"URL du serveur",
"login_form_failed_get_oauth_server_disable": "La fonctionnalité OAuth n'est pas disponible sur ce serveur",
"login_form_failed_login": "Erreur de connexion, vérifiez l'url du serveur, l'email et le mot de passe",
"login_form_handshake_exception": "Il y a eu une exception de liaison avec le serveur. Activez la prise en charge des certificats auto-signés dans les paramètres si vous utilisez un certificat auto-signé.",
"login_form_label_email": "E-mail",
"login_form_label_password": "Mot de passe",
"login_form_next_button": "Suivant",
"login_form_password_hint": "mot de passe",
"login_form_save_login": "Rester connecté",
"login_form_server_empty": "Saisissez l'URL du serveur.",
"login_form_server_error": "Impossible de se connecter au serveur.",
"login_password_changed_error": "Une erreur s'est produite lors de la mise à jour de votre mot de passe",
"login_password_changed_success": "Mot de passe mis à jour avec succès",
"map_cannot_get_user_location": "Impossible d'obtenir la localisation de l'utilisateur",
"map_location_dialog_cancel": "Annuler",
"map_location_dialog_yes": "Oui",
"map_location_service_disabled_content": "Le service de localisation doit être activé pour afficher les éléments de votre emplacement actuel. Souhaitez-vous l'activer maintenant?",
"map_location_service_disabled_title": "Service de localisation désactivé",
"map_no_assets_in_bounds": "Pas de photos dans cette zone",
"map_no_location_permission_content": "L'autorisation de localisation est nécessaire pour afficher les éléments de votre emplacement actuel. Souhaitez-vous l'autoriser maintenant?",
"map_no_location_permission_title": "Permission de localisation refusée",
"map_settings_dark_mode": "Mode sombre",
"map_settings_dialog_cancel": "Annuler",
"map_settings_dialog_save": "Sauvegarder",
"map_settings_dialog_title": "Paramètres de la carte",
"map_settings_include_show_archived": "Inclure les archives",
"map_settings_only_relative_range": "Plage de dates",
"map_settings_only_show_favorites": "Afficher uniquement les favoris",
"map_zoom_to_see_photos": "Dézoomer pour voir les photos",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Photos avec mouvement",
"notification_permission_dialog_cancel": "Annuler",
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
"notification_permission_dialog_settings": "Paramètres",
"notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.",
"notification_permission_list_tile_enable_button": "Activer les notifications",
"notification_permission_list_tile_title": "Permission de notification",
"partner_page_add_partner": "Ajouter un partenaire",
"partner_page_empty_message": "Vos photos ne sont pas encore partagées avec un partenaire.",
"partner_page_no_more_users": "Plus d'utilisateurs à ajouter",
"partner_page_partner_add_failed": "Échec de l'ajout d'un partenaire",
"partner_page_select_partner": "Sélectionner un partenaire",
"partner_page_shared_to_title": "Partagé avec",
"partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.",
"partner_page_stop_sharing_title": "Arrêter de partager vos photos?",
"partner_page_title": "Partenaire",
"permission_onboarding_continue_anyway": "Continuer quand même",
"permission_onboarding_get_started": "Commencer",
"permission_onboarding_go_to_settings": "Accéder aux paramètres",
"permission_onboarding_grant_permission": "Accorder l'autorisation",
"permission_onboarding_log_out": "Se déconnecter",
"permission_onboarding_permission_denied": "Permission refusée. Pour utiliser Immich, accordez lautorisation pour les photos et vidéos dans les Paramètres.",
"permission_onboarding_permission_granted": "Permission accordée! Vous êtes prêts.",
"permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.",
"permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo",
"profile_drawer_app_logs": "Journaux",
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
"profile_drawer_documentation": "Documentation",
"profile_drawer_github": "GitHub",
"profile_drawer_settings": "Paramètres",
"profile_drawer_sign_out": "Se déconnecter",
"profile_drawer_trash": "Corbeille",
"recently_added_page_title": "Récemment ajouté",
"search_bar_hint": "Rechercher vos photos",
"search_page_categories": "Catégories",
"search_page_favorites": "Favoris",
"search_page_motion_photos": "Photos avec mouvement",
"search_page_no_objects": "Aucune information disponible sur les objets",
"search_page_no_places": "Aucune information disponible sur la localisation",
"search_page_people": "Personnes",
"search_page_places": "Lieux",
"search_page_recently_added": "Récemment ajouté",
"search_page_screenshots": "Captures d'écran",
"search_page_selfies": "Selfies",
"search_page_things": "Objets",
"search_page_videos": "Vidéos",
"search_page_view_all_button": "Voir tout",
"search_page_your_activity": "Votre activité",
"search_result_page_new_search_hint": "Nouvelle recherche",
"search_suggestion_list_smart_search_hint_1": "La recherche intelligente est activée par défaut. Pour rechercher des métadonnées, utilisez la syntaxe suivante",
"search_suggestion_list_smart_search_hint_2": "m:votre-terme-de-recherche",
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"server_info_box_app_version": "Version de l'application",
"server_info_box_server_url": "URL du serveur",
"server_info_box_server_version": "Version du serveur",
"setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite vignette, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).",
"setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image en résolution originale (volumineux!). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).",
"setting_image_viewer_original_title": "Charger l'image originale",
"setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la vignette.",
"setting_image_viewer_preview_title": "Charger l'image d'aperçu",
"setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}",
"setting_notifications_notify_hours": "{} heures",
"setting_notifications_notify_immediately": "immédiatement",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "jamais",
"setting_notifications_notify_seconds": "{} secondes",
"setting_notifications_single_progress_subtitle": "Informations détaillées sur la progression du transfert par élément",
"setting_notifications_single_progress_title": "Afficher la progression du détail de la sauvegarde en arrière-plan",
"setting_notifications_subtitle": "Ajustez vos préférences de notification",
"setting_notifications_title": "Notifications",
"setting_notifications_total_progress_subtitle": "Progression globale du transfert (effectué/total des éléments)",
"setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan",
"setting_pages_app_bar_settings": "Paramètres",
"settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre",
"share_add": "Ajouter",
"share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre",
"share_create_album": "Créer un album",
"shared_album_activities_input_disable": "Les commentaires sont désactivés",
"shared_album_activities_input_hint": "Dire quelque chose",
"shared_album_activity_remove_content": "Souhaitez-vous supprimer cette activité?",
"shared_album_activity_remove_title": "Supprimer l'activité",
"shared_album_activity_setting_subtitle": "Laisser les autres réagir",
"shared_album_activity_setting_title": "Commentaires et likes",
"share_dialog_preparing": "Préparation...",
"shared_link_app_bar_title": "Liens partagés",
"shared_link_create_app_bar_title": "Créer un lien pour partager",
"shared_link_create_info": "Permettre à toute personne ayant le lien de voir la ou les photos sélectionnées",
"shared_link_create_submit_button": "Créer le lien",
"shared_link_edit_allow_download": "Autoriser les utilisateurs publics à télécharger",
"shared_link_edit_allow_upload": "Autoriser les utilisateurs publics à téléverser",
"shared_link_edit_app_bar_title": "Modifier le lien",
"shared_link_edit_change_expiry": "Modifier le délai d'expiration",
"shared_link_edit_description": "Description",
"shared_link_edit_description_hint": "Saisir la description du partage",
"shared_link_edit_expire_after": "Expire après",
"shared_link_edit_password": "Mot de passe",
"shared_link_edit_password_hint": "Saisir le mot de passe de partage",
"shared_link_edit_show_meta": "Afficher les métadonnées",
"shared_link_edit_submit_button": "Mettre à jour le lien",
"shared_link_empty": "Vous n'avez pas de liens partagés",
"shared_link_manage_links": "Gérer les liens partagés",
"share_done": "Fait",
"share_invite": "Inviter à l'album",
"sharing_page_album": "Albums partagés",
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
"sharing_page_empty_list": "LISTE VIDE",
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
"sharing_silver_appbar_shared_links": "Liens partagés",
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
"tab_controller_nav_library": "Bibliothèque",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Recherche",
"tab_controller_nav_sharing": "Partage",
"theme_setting_asset_list_storage_indicator_title": "Afficher l'indicateur de stockage sur les tuiles des éléments",
"theme_setting_asset_list_tiles_per_row_title": "Nombre d'éléments par ligne ({})",
"theme_setting_dark_mode_switch": "Mode sombre",
"theme_setting_image_viewer_quality_subtitle": "Ajustez la qualité de la visionneuse d'images détaillées",
"theme_setting_image_viewer_quality_title": "Qualité de la visualisation des images",
"theme_setting_system_theme_switch": "Automatique (suivre les paramètres du système)",
"theme_setting_theme_subtitle": "Choisissez le thème de l'application",
"theme_setting_theme_title": "Thème",
"theme_setting_three_stage_loading_subtitle": "Le chargement en trois étapes peut améliorer les performances de chargement, mais entraîne une augmentation significative de la charge du réseau.",
"theme_setting_three_stage_loading_title": "Activer le chargement en trois étapes",
"translated_text_options": "Options",
"trash_page_delete": "Supprimer",
"trash_page_delete_all": "Tout supprimer",
"trash_page_empty_trash_btn": "Vider la corbeille",
"trash_page_empty_trash_dialog_content": "Voulez-vous vider les éléments de la corbeille? Ces objets seront définitivement retirés d'Immich",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_info": "Les éléments mis à la corbeille seront définitivement supprimés au bout de {} jours.",
"trash_page_no_assets": "Pas d'éléments dans la corbeille",
"trash_page_restore": "Restaurer",
"trash_page_restore_all": "Tout restaurer",
"trash_page_select_assets_btn": "Sélectionner les éléments",
"trash_page_select_btn": "Sélectionner",
"trash_page_title": "Corbeille ({})",
"upload_dialog_cancel": "Annuler",
"upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur?",
"upload_dialog_ok": "Télécharger ",
"upload_dialog_title": "Télécharger cet élément ",
"version_announcement_overlay_ack": "Confirmer",
"version_announcement_overlay_release_notes": "notes de mise à jour",
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
"version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
"version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
"viewer_remove_from_stack": "Retirer de la pile",
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
"viewer_unstack": "Désempiler"
}
+1 -1
View File
@@ -169,4 +169,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.11.3
COCOAPODS: 1.12.1
-1
View File
@@ -28,7 +28,6 @@
<string>es</string>
<string>vi</string>
<string>fr</string>
<string>fr</string>
<string>ja</string>
<string>pl</string>
<string>fi</string>
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.86.0"
version_number: "1.85.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
-1
View File
@@ -9,7 +9,6 @@ const List<Locale> locales = [
Locale('it', 'IT'),
Locale('es', 'ES'),
Locale('vi', 'VN'),
Locale('fr', 'CA'),
Locale('fr', 'FR'),
Locale('ja', 'JP'),
Locale('pl', 'PL'),
@@ -274,7 +274,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// The app is currently in background. Perform the necessary cleanups which
// are on-hold for upload completion
if (appState != AppStateEnum.active && appState != AppStateEnum.resumed) {
ref.read(backupProvider.notifier).cancelBackup();
ref.read(appStateProvider.notifier).handleAppInactivity();
}
}
@@ -150,7 +150,6 @@ class ControlBottomAppBar extends ConsumerWidget {
SizedBox(
height: 70,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: renderActionButtons(),
),
+1 -2
View File
@@ -60,7 +60,7 @@ class User {
bool isPartnerSharedWith;
bool isAdmin;
String profileImagePath;
@Enumerated(EnumType.ordinal)
@Enumerated(EnumType.name)
AvatarColorEnum avatarColor;
bool memoryEnabled;
bool inTimeline;
@@ -103,7 +103,6 @@ class User {
}
enum AvatarColorEnum {
// do not change this order or reuse indices for other purposes, adding is OK
primary,
pink,
red,
+106 -27
View File
@@ -20,7 +20,7 @@ const UserSchema = CollectionSchema(
r'avatarColor': PropertySchema(
id: 0,
name: r'avatarColor',
type: IsarType.byte,
type: IsarType.string,
enumMap: _UseravatarColorEnumValueMap,
),
r'email': PropertySchema(
@@ -123,6 +123,7 @@ int _userEstimateSize(
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.avatarColor.name.length * 3;
bytesCount += 3 + object.email.length * 3;
bytesCount += 3 + object.id.length * 3;
bytesCount += 3 + object.name.length * 3;
@@ -136,7 +137,7 @@ void _userSerialize(
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeByte(offsets[0], object.avatarColor.index);
writer.writeString(offsets[0], object.avatarColor.name);
writer.writeString(offsets[1], object.email);
writer.writeString(offsets[2], object.id);
writer.writeBool(offsets[3], object.inTimeline);
@@ -157,7 +158,7 @@ User _userDeserialize(
) {
final object = User(
avatarColor:
_UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ??
_UseravatarColorValueEnumMap[reader.readStringOrNull(offsets[0])] ??
AvatarColorEnum.primary,
email: reader.readString(offsets[1]),
id: reader.readString(offsets[2]),
@@ -181,7 +182,7 @@ P _userDeserializeProp<P>(
) {
switch (propertyId) {
case 0:
return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ??
return (_UseravatarColorValueEnumMap[reader.readStringOrNull(offset)] ??
AvatarColorEnum.primary) as P;
case 1:
return (reader.readString(offset)) as P;
@@ -209,28 +210,28 @@ P _userDeserializeProp<P>(
}
const _UseravatarColorEnumValueMap = {
'primary': 0,
'pink': 1,
'red': 2,
'yellow': 3,
'blue': 4,
'green': 5,
'purple': 6,
'orange': 7,
'gray': 8,
'amber': 9,
r'primary': r'primary',
r'pink': r'pink',
r'red': r'red',
r'yellow': r'yellow',
r'blue': r'blue',
r'green': r'green',
r'purple': r'purple',
r'orange': r'orange',
r'gray': r'gray',
r'amber': r'amber',
};
const _UseravatarColorValueEnumMap = {
0: AvatarColorEnum.primary,
1: AvatarColorEnum.pink,
2: AvatarColorEnum.red,
3: AvatarColorEnum.yellow,
4: AvatarColorEnum.blue,
5: AvatarColorEnum.green,
6: AvatarColorEnum.purple,
7: AvatarColorEnum.orange,
8: AvatarColorEnum.gray,
9: AvatarColorEnum.amber,
r'primary': AvatarColorEnum.primary,
r'pink': AvatarColorEnum.pink,
r'red': AvatarColorEnum.red,
r'yellow': AvatarColorEnum.yellow,
r'blue': AvatarColorEnum.blue,
r'green': AvatarColorEnum.green,
r'purple': AvatarColorEnum.purple,
r'orange': AvatarColorEnum.orange,
r'gray': AvatarColorEnum.gray,
r'amber': AvatarColorEnum.amber,
};
Id _userGetId(User object) {
@@ -421,11 +422,14 @@ extension UserQueryWhere on QueryBuilder<User, User, QWhereClause> {
extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEqualTo(
AvatarColorEnum value) {
AvatarColorEnum value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
@@ -433,12 +437,14 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> avatarColorGreaterThan(
AvatarColorEnum value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
@@ -446,12 +452,14 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> avatarColorLessThan(
AvatarColorEnum value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
@@ -461,6 +469,7 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
AvatarColorEnum upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
@@ -469,6 +478,75 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'avatarColor',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'avatarColor',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'avatarColor',
value: '',
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'avatarColor',
value: '',
));
});
}
@@ -1538,9 +1616,10 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
}
extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
QueryBuilder<User, User, QDistinct> distinctByAvatarColor() {
QueryBuilder<User, User, QDistinct> distinctByAvatarColor(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'avatarColor');
return query.addDistinctBy(r'avatarColor', caseSensitive: caseSensitive);
});
}
@@ -28,10 +28,13 @@ enum AppStateEnum {
}
class AppStateNotiifer extends StateNotifier<AppStateEnum> {
final Ref _ref;
bool _wasPaused = false;
final Ref ref;
AppStateNotiifer(this._ref) : super(AppStateEnum.active);
AppStateNotiifer(this.ref) : super(AppStateEnum.active);
void updateAppState(AppStateEnum appState) {
state = appState;
}
AppStateEnum getAppState() {
return state;
@@ -40,72 +43,65 @@ class AppStateNotiifer extends StateNotifier<AppStateEnum> {
void handleAppResume() {
state = AppStateEnum.resumed;
// no need to resume because app was never really paused
if (!_wasPaused) return;
_wasPaused = false;
final isAuthenticated = _ref.read(authenticationProvider).isAuthenticated;
final permission = _ref.read(galleryPermissionNotifier);
var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated;
final permission = ref.watch(galleryPermissionNotifier);
// Needs to be logged in and have gallery permissions
if (isAuthenticated && (permission.isGranted || permission.isLimited)) {
_ref.read(backupProvider.notifier).resumeBackup();
_ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
_ref.read(serverInfoProvider.notifier).getServerVersion();
switch (_ref.read(tabProvider)) {
ref.read(backupProvider.notifier).resumeBackup();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
ref.read(serverInfoProvider.notifier).getServerVersion();
switch (ref.read(tabProvider)) {
case TabEnum.home:
_ref.read(assetProvider.notifier).getAllAsset();
_ref.read(assetProvider.notifier).getPartnerAssets();
ref.read(assetProvider.notifier).getAllAsset();
ref.read(assetProvider.notifier).getPartnerAssets();
case TabEnum.search:
// nothing to do
case TabEnum.sharing:
_ref.read(assetProvider.notifier).getPartnerAssets();
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.read(assetProvider.notifier).getPartnerAssets();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
case TabEnum.library:
_ref.read(albumProvider.notifier).getAllAlbums();
ref.read(albumProvider.notifier).getAllAlbums();
}
}
_ref.read(websocketProvider.notifier).connect();
ref.watch(websocketProvider.notifier).connect();
_ref.read(releaseInfoProvider.notifier).checkGithubReleaseInfo();
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
_ref
.read(notificationPermissionProvider.notifier)
ref
.watch(notificationPermissionProvider.notifier)
.getNotificationPermission();
_ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
ref.watch(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
_ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
_ref.invalidate(memoryFutureProvider);
ref.invalidate(memoryFutureProvider);
}
void handleAppInactivity() {
state = AppStateEnum.inactive;
// do not stop/clean up anything on inactivity: issued on every orientation change
// Do not handle inactivity if manual upload is in progress
if (ref.watch(backupProvider.notifier).backupProgress !=
BackUpProgressEnum.manualInProgress) {
ImmichLogger().flush();
ref.read(websocketProvider.notifier).disconnect();
ref.read(backupProvider.notifier).cancelBackup();
}
}
void handleAppPause() {
state = AppStateEnum.paused;
_wasPaused = true;
// Do not cancel backup if manual upload is in progress
if (_ref.read(backupProvider.notifier).backupProgress !=
BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup();
}
_ref.read(websocketProvider.notifier).disconnect();
ImmichLogger().flush();
}
void handleAppDetached() {
state = AppStateEnum.detached;
// no guarantee this is called at all
_ref.read(manualUploadProvider.notifier).cancelBackup();
ref.watch(manualUploadProvider.notifier).cancelBackup();
}
void handleAppHidden() {
state = AppStateEnum.hidden;
// do not stop/clean up anything on inactivity: issued on every orientation change
}
}
@@ -63,19 +63,21 @@ class WebsocketState {
}
class WebsocketNotifier extends StateNotifier<WebsocketState> {
WebsocketNotifier(this._ref)
WebsocketNotifier(this.ref)
: super(
WebsocketState(socket: null, isConnected: false, pendingChanges: []),
);
) {
debounce = Debounce(
const Duration(milliseconds: 500),
);
}
final _log = Logger('WebsocketNotifier');
final Ref _ref;
final Debounce _debounce = Debounce(const Duration(milliseconds: 500));
final log = Logger('WebsocketNotifier');
final Ref ref;
late final Debounce debounce;
/// Connects websocket to server unless already connected
void connect() {
if (state.isConnected) return;
final authenticationState = _ref.read(authenticationProvider);
connect() {
var authenticationState = ref.read(authenticationProvider);
if (authenticationState.isAuthenticated) {
final accessToken = Store.get(StoreKey.accessToken);
@@ -116,7 +118,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
});
socket.on('error', (errorMessage) {
_log.severe("Websocket Error - $errorMessage");
log.severe("Websocket Error - $errorMessage");
state = WebsocketState(
isConnected: false,
socket: null,
@@ -136,7 +138,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
}
void disconnect() {
disconnect() {
debugPrint("Attempting to disconnect from websocket");
var socket = state.socket?.disconnect();
@@ -150,30 +152,30 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
}
void stopListenToEvent(String eventName) {
stopListenToEvent(String eventName) {
debugPrint("Stop listening to event $eventName");
state.socket?.off(eventName);
}
void listenUploadEvent() {
listenUploadEvent() {
debugPrint("Start listening to event on_upload_success");
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
}
void addPendingChange(PendingAction action, dynamic value) {
addPendingChange(PendingAction action, dynamic value) {
state = state.copyWith(
pendingChanges: [...state.pendingChanges, PendingChange(action, value)],
);
}
void handlePendingChanges() {
handlePendingChanges() {
final deleteChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetDelete)
.toList();
if (deleteChanges.isNotEmpty) {
List<String> remoteIds =
deleteChanges.map((a) => a.value.toString()).toList();
_ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
state = state.copyWith(
pendingChanges: state.pendingChanges
.where((c) => c.action != PendingAction.assetDelete)
@@ -182,27 +184,27 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
}
void _handleOnUploadSuccess(dynamic data) {
_handleOnUploadSuccess(dynamic data) {
final dto = AssetResponseDto.fromJson(data);
if (dto != null) {
final newAsset = Asset.remote(dto);
_ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
}
}
void _handleOnConfigUpdate(dynamic _) {
_ref.read(serverInfoProvider.notifier).getServerFeatures();
_ref.read(serverInfoProvider.notifier).getServerConfig();
_handleOnConfigUpdate(dynamic _) {
ref.read(serverInfoProvider.notifier).getServerFeatures();
ref.read(serverInfoProvider.notifier).getServerConfig();
}
// Refresh updated assets
void _handleServerUpdates(dynamic _) {
_ref.read(assetProvider.notifier).getAllAsset();
_handleServerUpdates(dynamic _) {
ref.read(assetProvider.notifier).getAllAsset();
}
void _handleOnAssetDelete(dynamic data) {
_handleOnAssetDelete(dynamic data) {
addPendingChange(PendingAction.assetDelete, data);
_debounce(handlePendingChanges);
debounce(handlePendingChanges);
}
}
+2 -3
View File
@@ -34,13 +34,12 @@ class ControlBoxButton extends StatelessWidget {
padding: const EdgeInsets.all(10),
shape: const CircleBorder(),
onPressed: onPressed,
minWidth: 75.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(iconData, size: 24),
const SizedBox(height: 4),
const SizedBox(height: 6),
Text(
label,
style: const TextStyle(fontSize: 12.0),
@@ -30,7 +30,6 @@ class UserCircleAvatar extends ConsumerWidget {
user.name[0].toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary
? Colors.black
: Colors.white,
+6 -19
View File
@@ -40,25 +40,12 @@ class SplashScreenPage extends HookConsumerWidget {
log.severe(e);
}
try {
isSuccess = await ref
.read(authenticationProvider.notifier)
.setSuccessLoginInfo(
accessToken: accessToken,
serverUrl: serverUrl,
offlineLogin: deviceIsOffline,
);
} catch (error, stackTrace) {
ref.read(authenticationProvider.notifier).logout();
log.severe(
'Cannot set success login info: $error',
error,
stackTrace,
);
context.autoPush(const LoginRoute());
}
isSuccess =
await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
accessToken: accessToken,
serverUrl: serverUrl,
offlineLogin: deviceIsOffline,
);
}
// If the device is offline and there is a currentUser stored locallly
+1 -1
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.86.0
- API version: 1.85.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.86.0+110
version: 1.85.0+109
isar_version: &isar_version 3.1.0+1
environment:
+7 -4
View File
@@ -5773,7 +5773,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.86.0",
"version": "1.85.0",
"contact": {}
},
"tags": [],
@@ -7656,7 +7656,8 @@
"PartnerResponseDto": {
"properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
"$ref": "#/components/schemas/UserAvatarColor",
"nullable": true
},
"createdAt": {
"format": "date-time",
@@ -9250,7 +9251,8 @@
"UserDto": {
"properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
"$ref": "#/components/schemas/UserAvatarColor",
"nullable": true
},
"email": {
"type": "string"
@@ -9277,7 +9279,8 @@
"UserResponseDto": {
"properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
"$ref": "#/components/schemas/UserAvatarColor",
"nullable": true
},
"createdAt": {
"format": "date-time",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.86.0",
"version": "1.85.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.86.0",
"version": "1.85.0",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.86.0",
"version": "1.85.0",
"description": "",
"author": "",
"private": true,
@@ -1,4 +1,5 @@
import { UserAvatarColor, UserEntity } from '@app/infra/entities';
import { Optional } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator';
@@ -19,8 +20,9 @@ export class UserDto {
email!: string;
profileImagePath!: string;
@IsEnum(UserAvatarColor)
@Optional()
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor!: UserAvatarColor;
avatarColor!: UserAvatarColor | null;
}
export class UserResponseDto extends UserDto {
+2 -2
View File
@@ -355,10 +355,10 @@ describe(UserService.name, () => {
});
describe('deleteProfileImage', () => {
it('should send an http error has no profile image', async () => {
it('should do nothing if the user has no profile image', async () => {
userMock.get.mockResolvedValue(userStub.admin);
await expect(sut.deleteProfileImage(userStub.admin)).rejects.toBeInstanceOf(BadRequestException);
await sut.deleteProfileImage(userStub.admin);
expect(jobMock.queue).not.toHaveBeenCalled();
});
+8 -6
View File
@@ -96,18 +96,20 @@ export class UserService {
const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false });
const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path });
if (oldpath !== '') {
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } });
const files = [oldpath];
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
}
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
}
async deleteProfileImage(authUser: AuthUserDto): Promise<void> {
const user = await this.findOrFail(authUser.id, { withDeleted: false });
if (user.profileImagePath === '') {
throw new BadRequestException("Can't delete a missing profile Image");
if (user.profileImagePath !== '') {
await this.userRepository.update(authUser.id, { profileImagePath: '' });
const files = [user.profileImagePath];
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
}
await this.userRepository.update(authUser.id, { profileImagePath: '' });
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } });
return;
}
async getProfileImage(id: string): Promise<ImmichReadStream> {
@@ -124,7 +126,7 @@ export class UserService {
throw new BadRequestException('Admin account does not exist');
}
const providedPassword = await ask(mapUser(admin));
const providedPassword = await ask(admin);
const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
await this.userCore.updateUser(admin, admin.id, { password });
@@ -12,7 +12,6 @@ import {
SignUpDto,
UserResponseDto,
ValidateAccessTokenResponseDto,
mapUser,
} from '@app/domain';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@@ -72,7 +71,7 @@ export class AuthController {
@Post('change-password')
@HttpCode(HttpStatus.OK)
changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(authUser, dto).then(mapUser);
return this.service.changePassword(authUser, dto);
}
@Post('logout')
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.86.0
* The version of the OpenAPI document: 1.85.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
-22
View File
@@ -2,28 +2,6 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* light */
--immich-primary: 66 80 175;
--immich-bg: 255 255 255;
--immich-fg: 0 0 0;
--immich-gray: 246 246 244;
--immich-error: 229 115 115;
--immich-success: 129 199 132;
--immich-warning: 255 183 77;
/* dark */
--immich-dark-primary: 172 203 250;
--immich-dark-bg: 0 0 0;
--immich-dark-fg: 229 231 235;
--immich-dark-gray: 33 33 33;
--immich-dark-error: 211 47 47;
--immich-dark-success: 56 142 60;
--immich-dark-warning: 245 124 0;
}
}
@font-face {
font-family: 'Work Sans';
src: url('$lib/assets/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations');
@@ -6,18 +6,10 @@
import { AlbumResponseDto, AssetResponseDto, ThumbnailFormat, api } from '@api';
import { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { slide } from 'svelte/transition';
import { asByteUnitString } from '../../utils/byte-units';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import {
mdiCalendar,
mdiCameraIris,
mdiClose,
mdiImageOutline,
mdiMapMarkerOutline,
mdiInformationOutline,
} from '@mdi/js';
import { mdiCalendar, mdiCameraIris, mdiClose, mdiImageOutline, mdiMapMarkerOutline } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import Map from '../shared-components/map/map.svelte';
@@ -85,9 +77,6 @@
console.error(error);
}
};
let showAssetPath = false;
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
</script>
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
@@ -226,15 +215,8 @@
<div><Icon path={mdiImageOutline} size="24" /></div>
<div>
<p class="break-all flex place-items-center gap-2">
{#if isOwner}
{asset.originalFileName}
<button title="Show File Location" on:click={toggleAssetPath}>
<Icon path={mdiInformationOutline} />
</button>
{:else}
{getAssetFilename(asset)}
{/if}
<p class="break-all">
{getAssetFilename(asset)}
</p>
<div class="flex gap-2 text-sm">
{#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
@@ -248,11 +230,6 @@
{/if}
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
</div>
{#if showAssetPath}
<p class="text-xs opacity-50 break-all" transition:slide={{ duration: 250 }}>
{asset.originalPath}
</p>
{/if}
</div>
</div>
{/if}
@@ -15,22 +15,24 @@
<FullScreenModal on:clickOutside={() => dispatch('close')} on:escape={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg p-4"
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div class="flex items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm">
SELECT AVATAR COLOR
<div class="flex px-2 pt-2 items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">
Select avatar color
</h1>
<div>
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
</div>
</div>
<div class="flex items-center justify-center p-4 mt-4">
<div class="flex items-center justify-center p-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
{#each colors as color}
<button on:click={() => dispatch('choose', color)}>
<UserAvatar {user} {color} size="xl" showProfileImage={false} />
</button>
<div>
<button on:click={() => dispatch('choose', color)}>
<UserAvatar {user} {color} size="xl" showProfileImage={false} />
</button>
</div>
{/each}
</div>
</div>
@@ -42,7 +42,7 @@
sm: 'w-7 h-7',
md: 'w-10 h-10',
lg: 'w-12 h-12',
xl: 'w-16 h-16',
xl: 'w-20 h-20',
xxl: 'w-24 h-24',
xxxl: 'w-28 h-28',
};
+14 -14
View File
@@ -6,22 +6,22 @@ module.exports = {
extend: {
colors: {
// Light Theme
'immich-primary': 'rgb(var(--immich-primary) / <alpha-value>)',
'immich-bg': 'rgb(var(--immich-bg) / <alpha-value>)',
'immich-fg': 'rgb(var(--immich-fg) / <alpha-value>)',
'immich-gray': 'rgb(var(--immich-gray) / <alpha-value>)',
'immich-error': 'rgb(var(--immich-error) / <alpha-value>)',
'immich-success': 'rgb(var(--immich-success) / <alpha-value>)',
'immich-warning': 'rgb(var(--immich-warning) / <alpha-value>)',
'immich-primary': '#4250af',
'immich-bg': 'white',
'immich-fg': 'black',
'immich-gray': '#F6F6F4',
'immich-error': '#e57373',
'immich-success': '#81c784',
'immich-warning': '#ffb74d',
// Dark Theme
'immich-dark-primary': 'rgb(var(--immich-dark-primary) / <alpha-value>)',
'immich-dark-bg': 'rgb(var(--immich-dark-bg) / <alpha-value>)',
'immich-dark-fg': 'rgb(var(--immich-dark-fg) / <alpha-value>)',
'immich-dark-gray': 'rgb(var(--immich-dark-gray) / <alpha-value>)',
'immich-dark-error': 'rgb(var(--immich-dark-error) / <alpha-value>)',
'immich-dark-success': 'rgb(var(--immich-dark-success) / <alpha-value>)',
'immich-dark-warning': 'rgb(var(--immich-dark-warning) / <alpha-value>)',
'immich-dark-primary': '#adcbfa',
'immich-dark-bg': 'black',
'immich-dark-fg': '#e5e7eb',
'immich-dark-gray': '#212121',
'immich-dark-error': '#d32f2f',
'immich-dark-success': '#388e3c',
'immich-dark-warning': '#f57c00',
},
fontFamily: {
'immich-title': ['Snowburst One', 'cursive'],